@gloom-sh/terminal 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +24 -6
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +27 -3
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/financials.ts +32 -1
- package/src/types/headless.ts +21 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PaneFooterSegment } from "../../../components";
|
|
2
|
+
import type { QueryBarSelectFilter } from "../../../components/ui/query-bar";
|
|
3
|
+
import { formatCompact } from "../../../utils/format";
|
|
4
|
+
|
|
5
|
+
/** No minimum: every row, with or without a float. */
|
|
6
|
+
export const ANY_FLOAT = "any";
|
|
7
|
+
/** The minimums the pane's menu offers; `--min-float` takes any other. */
|
|
8
|
+
const PRESETS = ["1M", "5M", "10M", "20M", "50M", "100M"] as const;
|
|
9
|
+
const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9 };
|
|
10
|
+
|
|
11
|
+
/** Shares in `text` (10M, 500k, 2.5M, 25000000), or null when it is not a share count. */
|
|
12
|
+
function parseShares(text: string): number | null {
|
|
13
|
+
const match = /^((?:\d+(?:\.\d*)?|\.\d+))([kmb])?$/i.exec(text.trim().replace(/[_,]/g, ""));
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const value = Number(match[1]) * (match[2] ? SCALE[match[2].toLowerCase()]! : 1);
|
|
16
|
+
return Number.isFinite(value) ? value : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The minimum a saved or typed value sets, in shares; null for none (`any`, 0 or a value that is not a count). */
|
|
20
|
+
export function minFloatShares(value: string | null | undefined): number | null {
|
|
21
|
+
if (!value || value.trim().toLowerCase() === ANY_FLOAT) return null;
|
|
22
|
+
const shares = parseShares(value);
|
|
23
|
+
return shares != null && shares > 0 ? shares : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `--min-float` as the pane keeps it: `10M` for 10m or 10000000, `any` for none. Throws on a value that is not a share count. */
|
|
27
|
+
export function normalizeMinFloat(value: string): string {
|
|
28
|
+
if (value.trim().toLowerCase() === ANY_FLOAT) return ANY_FLOAT;
|
|
29
|
+
const shares = parseShares(value);
|
|
30
|
+
if (shares == null) throw new Error(`--min-float takes a share count such as 10M or 500k, or any; got "${value}".`);
|
|
31
|
+
return shares > 0 ? formatCompact(shares) : ANY_FLOAT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A row passes with a float at or above the minimum; a row whose float is not known does not. */
|
|
35
|
+
export function meetsMinFloat(floatShares: number | null | undefined, minimum: number | null): boolean {
|
|
36
|
+
return minimum == null || (floatShares != null && floatShares >= minimum);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The pane's Min float menu: Any and the presets, with a minimum set from the CLI kept as its own choice. */
|
|
40
|
+
export function minFloatFilter(value: string, onChange: (value: string) => void): QueryBarSelectFilter {
|
|
41
|
+
const current = minFloatShares(value) == null ? ANY_FLOAT : value;
|
|
42
|
+
const presets: string[] = [...PRESETS];
|
|
43
|
+
const options = [ANY_FLOAT, ...(presets.includes(current) || current === ANY_FLOAT ? [] : [current]), ...presets]
|
|
44
|
+
.map((option) => ({ value: option, label: option === ANY_FLOAT ? "Any" : option }));
|
|
45
|
+
return {
|
|
46
|
+
id: "min-float",
|
|
47
|
+
label: "Min float",
|
|
48
|
+
title: "Minimum float",
|
|
49
|
+
value: current,
|
|
50
|
+
defaultValue: ANY_FLOAT,
|
|
51
|
+
options,
|
|
52
|
+
onChange,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a list says under its Float column while a shown float is partial. */
|
|
57
|
+
export const PARTIAL_FLOAT_NOTE = "Float is partial: the source does not say when it measured it.";
|
|
58
|
+
|
|
59
|
+
/** Whether any row's float is partial, which the list then says once. */
|
|
60
|
+
export function hasPartialFloat(rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): boolean {
|
|
61
|
+
return rows.some((row) => row.floatShares != null && row.floatSharesState === "partial");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The footer's word for a FLOAT column of partial floats, or none. */
|
|
65
|
+
export function partialFloatFooter(shown: boolean, rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): PaneFooterSegment[] {
|
|
66
|
+
return shown && hasPartialFloat(rows)
|
|
67
|
+
? [{ id: "float-partial", parts: [{ text: "FLOAT", tone: "label" }, { text: "partial, undated", tone: "muted" }] }]
|
|
68
|
+
: [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The empty list's title while a minimum hides every row. */
|
|
72
|
+
export function minFloatEmptyTitle(value: string): string {
|
|
73
|
+
return `No movers with a float of ${value} or more.`;
|
|
74
|
+
}
|
|
@@ -50,7 +50,8 @@ type MarketMoverColumnId =
|
|
|
50
50
|
| "volume"
|
|
51
51
|
| "volumeRatio"
|
|
52
52
|
| "range"
|
|
53
|
-
| "marketCap"
|
|
53
|
+
| "marketCap"
|
|
54
|
+
| "floatShares";
|
|
54
55
|
export type MarketMoverColumn = DataTableColumn & { id: MarketMoverColumnId };
|
|
55
56
|
/**
|
|
56
57
|
* A list row. Price and change are the regular session's, as `ticker` reads
|
|
@@ -123,6 +124,8 @@ function getSortValue(
|
|
|
123
124
|
return fiftyTwoWeekPositionPercent(row.price, row.fiftyTwoWeekLow, row.fiftyTwoWeekHigh);
|
|
124
125
|
case "marketCap":
|
|
125
126
|
return row.marketCap ?? null;
|
|
127
|
+
case "floatShares":
|
|
128
|
+
return row.floatShares ?? null;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -220,6 +220,17 @@ function parseScreenerSessions(row: Record<string, unknown>): CloudMoverSessionF
|
|
|
220
220
|
return sessions;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** A positive float with its state, or nothing: a row without one fails any minimum. */
|
|
224
|
+
function floatOf(source: { floatShares?: unknown; floatSharesState?: unknown; floatSharesReason?: unknown } | null | undefined): Pick<ScreenerQuote, "floatShares" | "floatSharesState" | "floatSharesReason"> {
|
|
225
|
+
const shares = screenerNumber(source?.floatShares);
|
|
226
|
+
if (shares == null || !(shares > 0)) return {};
|
|
227
|
+
return {
|
|
228
|
+
floatShares: shares,
|
|
229
|
+
...(source?.floatSharesState === "partial" ? { floatSharesState: "partial" as const } : {}),
|
|
230
|
+
...(source?.floatSharesState === "partial" && typeof source.floatSharesReason === "string" ? { floatSharesReason: source.floatSharesReason } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
223
234
|
/** Validate numeric fields at the API boundary so absent values stay unavailable. */
|
|
224
235
|
export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
|
|
225
236
|
if (!data || typeof data !== "object" || !("quotes" in data) || !Array.isArray(data.quotes)) {
|
|
@@ -239,6 +250,7 @@ export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
|
|
|
239
250
|
previousClose: screenerNumber(q.previousClose) ?? undefined, exchange: typeof q.exchange === "string" ? q.exchange : "",
|
|
240
251
|
lastUpdated: typeof q.lastUpdated === "number" && Number.isFinite(q.lastUpdated) && q.lastUpdated > 0 ? q.lastUpdated : undefined,
|
|
241
252
|
sessions: parseScreenerSessions(q),
|
|
253
|
+
...floatOf(q),
|
|
242
254
|
}];
|
|
243
255
|
});
|
|
244
256
|
}
|
|
@@ -325,6 +337,8 @@ function mergeCloudScreenerItem(
|
|
|
325
337
|
lastUpdated: item.lastUpdated,
|
|
326
338
|
previousClose: screenerNumber(item.previousClose) ?? undefined,
|
|
327
339
|
sessions: parseScreenerSessions(item as unknown as Record<string, unknown>),
|
|
340
|
+
// The live list's own float, the delayed list's when it sends none, each with its own state.
|
|
341
|
+
...floatOf(item.floatShares != null ? item : metadata),
|
|
328
342
|
};
|
|
329
343
|
}
|
|
330
344
|
|
|
@@ -20,6 +20,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
|
|
|
20
20
|
import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
21
21
|
import { ProWall, SignInWall } from "../cloud/auth-actions";
|
|
22
22
|
import { loadSessionMovers, SessionMoversAccessError } from "./client";
|
|
23
|
+
import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
|
|
23
24
|
import { summaryFooterSegments } from "./footer";
|
|
24
25
|
import type { MarketSummaryQuote } from "./screener";
|
|
25
26
|
import {
|
|
@@ -104,7 +105,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
104
105
|
const { pinTicker } = usePluginTickerActions();
|
|
105
106
|
const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
|
|
106
107
|
const side = resolveSide(view, savedSide);
|
|
107
|
-
const
|
|
108
|
+
const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
|
|
109
|
+
const minFloat = minFloatShares(minFloatValue);
|
|
110
|
+
const listKey = `${view}:${side}:${minFloat ?? ""}`;
|
|
108
111
|
const [loaded, setLoaded] = useState<LoadedList | null>(null);
|
|
109
112
|
// The first frame would otherwise claim an empty list.
|
|
110
113
|
const [loading, setLoading] = useState(true);
|
|
@@ -132,9 +135,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
132
135
|
setLoadError(null);
|
|
133
136
|
}
|
|
134
137
|
try {
|
|
135
|
-
const next = await loadSessionMovers(view, side, options);
|
|
138
|
+
const next = await loadSessionMovers(view, side, { ...options, minFloat });
|
|
136
139
|
if (fetchGenRef.current !== gen) return;
|
|
137
|
-
setLoaded({ key: `${view}:${side}`, payload: next });
|
|
140
|
+
setLoaded({ key: `${view}:${side}:${minFloat ?? ""}`, payload: next });
|
|
138
141
|
setLoadError(null);
|
|
139
142
|
setLastLoadedAt(Date.now());
|
|
140
143
|
} catch (error) {
|
|
@@ -147,7 +150,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
147
150
|
} finally {
|
|
148
151
|
if (fetchGenRef.current === gen && !options?.background) setLoading(false);
|
|
149
152
|
}
|
|
150
|
-
}, [onDenied, side, view]);
|
|
153
|
+
}, [minFloat, onDenied, side, view]);
|
|
151
154
|
|
|
152
155
|
useEffect(() => {
|
|
153
156
|
setSelectedId(null);
|
|
@@ -167,7 +170,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
167
170
|
freshnessScopeKey: `market-movers:${listKey}`,
|
|
168
171
|
liveStreaming,
|
|
169
172
|
});
|
|
170
|
-
const columns = useMemo(() => buildSessionMoverColumns(view, width), [view, width]);
|
|
173
|
+
const columns = useMemo(() => buildSessionMoverColumns(view, width, { float: minFloat != null }), [minFloat, view, width]);
|
|
171
174
|
// The sort survives a change of list, but not onto a column this list or width does not show.
|
|
172
175
|
const sort = columns.some((column) => column.id === sortPreference.columnId) ? sortPreference : DEFAULT_SORT;
|
|
173
176
|
const rows = useMemo(
|
|
@@ -199,8 +202,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
199
202
|
parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
|
|
200
203
|
}] : []),
|
|
201
204
|
...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
|
|
205
|
+
// A float the source does not date says so once, while the FLOAT column shows.
|
|
206
|
+
...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
|
|
202
207
|
],
|
|
203
|
-
}), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
|
|
208
|
+
}), [asOfLabel, columns, feedStatus, loadError, loading, payload?.stale, rows, sessionLabel, summaryQuotes]);
|
|
204
209
|
|
|
205
210
|
const openRow = useCallback((row: SessionMoverRow) => {
|
|
206
211
|
pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
|
|
@@ -217,7 +222,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
217
222
|
const sides = sessionSides(view);
|
|
218
223
|
// Until this list's first answer the table is loading, including the frame
|
|
219
224
|
// between a change of list and its request going out.
|
|
220
|
-
const emptyTitle = payload ? "No movers yet." : loadError ?? "Loading movers...";
|
|
225
|
+
const emptyTitle = payload ? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers yet.") : loadError ?? "Loading movers...";
|
|
221
226
|
return (
|
|
222
227
|
<DataTableView<SessionMoverRow, SessionMoverColumn>
|
|
223
228
|
focused={focused}
|
|
@@ -231,7 +236,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
231
236
|
value: side,
|
|
232
237
|
options: sides,
|
|
233
238
|
onChange: (value: CloudSessionMoversSide) => setSavedSide(value),
|
|
234
|
-
}]}
|
|
239
|
+
}, minFloatFilter(minFloatValue, setMinFloatValue)]}
|
|
235
240
|
/>
|
|
236
241
|
)}
|
|
237
242
|
selection={{ kind: "id", selectedId, getId: rowKey, onChange: setSelectedId }}
|
|
@@ -21,9 +21,10 @@ const VOLUME_LABEL: Record<CloudSessionMoversCategory, string> = {
|
|
|
21
21
|
* Pre-market and after hours: last, change from the reference close, session
|
|
22
22
|
* volume, relative volume, float and the catalyst. Gaps add the gap and the
|
|
23
23
|
* distance from VWAP. The name gives way first, then the columns a narrow pane
|
|
24
|
-
* can do without (the day's volume, then the float
|
|
24
|
+
* can do without (the day's volume, then the float, which stays while a
|
|
25
|
+
* minimum float is set: `float`).
|
|
25
26
|
*/
|
|
26
|
-
export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number): SessionMoverColumn[] {
|
|
27
|
+
export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number, options: { float?: boolean } = {}): SessionMoverColumn[] {
|
|
27
28
|
const columns: SessionMoverColumn[] = [
|
|
28
29
|
{ id: "rank", label: "#", width: 3, align: "left" },
|
|
29
30
|
{ id: "symbol", label: "TICKER", width: 8, align: "left" },
|
|
@@ -37,7 +38,7 @@ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width
|
|
|
37
38
|
{ id: "floatShares", label: "FLOAT", width: 7, align: "right" },
|
|
38
39
|
{ id: "catalyst", label: "EVENT", width: 6, align: "left" },
|
|
39
40
|
];
|
|
40
|
-
const optional: SessionMoverColumnId[] = view === "gaps" ? ["volume",
|
|
41
|
+
const optional: SessionMoverColumnId[] = [...(view === "gaps" ? ["volume" as const] : []), ...(options.float ? [] : ["floatShares" as const])];
|
|
41
42
|
// Measured the way the table draws it: a header widens its column, and a
|
|
42
43
|
// left-aligned column after a right-aligned one gets a lead gap. The name
|
|
43
44
|
// sits between left-aligned columns, so it adds only its width and a gutter.
|
|
@@ -26,15 +26,20 @@ function fiftyTwoWeekPosition(price: number | null, low: number | undefined, hig
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const EXTENDED_COLUMN_WIDTH = 9;
|
|
29
|
-
/** With an extended column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
|
|
29
|
+
/** With an extended or float column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
|
|
30
30
|
const EXTENDED_MIN_NAME_WIDTH = 12;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* The list's columns at `width`. A pre-market or after-hours column follows
|
|
34
|
-
* CHG% for each session in `extendedSessions
|
|
35
|
-
*
|
|
34
|
+
* CHG% for each session in `extendedSessions`, and FLOAT ends the row while a
|
|
35
|
+
* minimum float is set (`float`); to make room for them a narrow pane gives up
|
|
36
|
+
* MCAP, then 52W%, before it cuts the names short.
|
|
36
37
|
*/
|
|
37
|
-
export function buildMarketMoverColumns(
|
|
38
|
+
export function buildMarketMoverColumns(
|
|
39
|
+
width: number,
|
|
40
|
+
extendedSessions: readonly ExtendedSession[] = [],
|
|
41
|
+
options: { float?: boolean } = {},
|
|
42
|
+
): MarketMoverColumn[] {
|
|
38
43
|
const lead: MarketMoverColumn[] = [
|
|
39
44
|
{ id: "rank", label: "#", width: 3, align: "left" },
|
|
40
45
|
{ id: "symbol", label: "TICKER", width: 8, align: "left" },
|
|
@@ -55,12 +60,13 @@ export function buildMarketMoverColumns(width: number, extendedSessions: readonl
|
|
|
55
60
|
{ id: "volumeRatio", label: "V/AVG", width: 6, align: "right" },
|
|
56
61
|
{ id: "range", label: "52W%", width: 6, align: "right" },
|
|
57
62
|
{ id: "marketCap", label: "MCAP", width: 8, align: "right" },
|
|
63
|
+
...(options.float ? [{ id: "floatShares" as const, label: "FLOAT", width: 7, align: "right" as const }] : []),
|
|
58
64
|
];
|
|
59
65
|
// Measured the way the table draws them (a header and its sort mark widen a
|
|
60
66
|
// column, gaps between them); the name adds its own width and a gap.
|
|
61
67
|
const nameRoom = () => width - getTableWidth([...lead, ...quote, ...trailing]) - 1;
|
|
62
68
|
for (const id of ["marketCap", "range"] as const) {
|
|
63
|
-
if (extendedSessions.length === 0 || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
|
|
69
|
+
if ((extendedSessions.length === 0 && !options.float) || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
|
|
64
70
|
trailing.splice(trailing.findIndex((column) => column.id === id), 1);
|
|
65
71
|
}
|
|
66
72
|
return [...lead, { id: "name", label: "NAME", width: Math.max(6, nameRoom()), align: "left" }, ...quote, ...trailing];
|
|
@@ -121,5 +127,11 @@ export function renderMarketMoverCell(
|
|
|
121
127
|
value: row.marketCap,
|
|
122
128
|
color: colors.textDim,
|
|
123
129
|
};
|
|
130
|
+
case "floatShares":
|
|
131
|
+
return {
|
|
132
|
+
text: row.floatShares != null ? formatCompact(row.floatShares, { fixedDecimals: true }) : "—",
|
|
133
|
+
value: row.floatShares ?? null,
|
|
134
|
+
color: colors.textDim,
|
|
135
|
+
};
|
|
124
136
|
}
|
|
125
137
|
}
|
|
@@ -40,7 +40,8 @@ export function Breadth({ fund, width, height, focused, active }: Pick<PaneProps
|
|
|
40
40
|
color: colors.warning, calendarSpaced: true,
|
|
41
41
|
})], [history, metric, colors.warning]);
|
|
42
42
|
const link = useChartTableSelection({ rows, getId: keyOf, getDate: dateOf, selectedId: selected ?? latest?.date ?? null, onSelect: setSelected, focused });
|
|
43
|
-
|
|
43
|
+
// The resource keeps a failed load as its error (stale, and a warning below), so this refresh never rejects.
|
|
44
|
+
const sessionOpen = useLiveSessionRefresh(() => resource.load(true), data?.access === "realtime" ? 15_000 : 60_000, active && visible, resource.updatedAt).live;
|
|
44
45
|
useAutoRefresh(resource.updatedAt, () => { if (active && visible && !sessionOpen) void resource.load(data?.basis === "intraday"); });
|
|
45
46
|
usePaneRefreshKey(() => void resource.reload(), { focused, enabled: active });
|
|
46
47
|
usePaneStatusFooter({ registrationId: "members:breadth", loading: !!data && resource.loading,
|
|
@@ -112,6 +112,7 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
112
112
|
? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
|
|
113
113
|
: undefined,
|
|
114
114
|
onPopOut: readableArticle ? popOutReadable : undefined,
|
|
115
|
+
feedAccess: newsState.access,
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
if (!ticker) {
|
|
@@ -29,10 +29,13 @@ const CATEGORY_LABELS: Record<string, string> = {
|
|
|
29
29
|
us: "US",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
function categoryKey(value: string | null | undefined): string {
|
|
33
|
+
return value?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
export function formatNewsCategory(value: string | null | undefined): string {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
-
const key = raw.toLowerCase().replace(/[\s-]+/g, "_");
|
|
37
|
+
const key = categoryKey(value);
|
|
38
|
+
if (!key) return "";
|
|
36
39
|
const mapped = CATEGORY_LABELS[key];
|
|
37
40
|
if (mapped) return mapped;
|
|
38
41
|
const words = key.split("_").filter(Boolean);
|
|
@@ -46,6 +49,19 @@ export function formatNewsCategory(value: string | null | undefined): string {
|
|
|
46
49
|
.join(" ");
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Labels for the news tables' category column, which is ten cells wide, for the
|
|
54
|
+
* ids whose label above would be cut in it. Anywhere with room (a story's
|
|
55
|
+
* detail) keeps the full label.
|
|
56
|
+
*/
|
|
57
|
+
const COLUMN_CATEGORY_LABELS: Record<string, string> = {
|
|
58
|
+
product_approval: "Approval",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function formatNewsCategoryForColumn(value: string | null | undefined): string {
|
|
62
|
+
return COLUMN_CATEGORY_LABELS[categoryKey(value)] ?? formatNewsCategory(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
const KNOWN_TICKERS = new Set([
|
|
50
66
|
"AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "JPM", "JNJ", "UNH",
|
|
51
67
|
"PG", "XOM", "CVX", "HD", "BAC", "V", "MA", "PFE", "KO", "PEP", "ABBV", "MRK",
|
|
@@ -125,6 +125,7 @@ function IndustryNewsBody({ focused, width, height, entry, code, codeFilter }: P
|
|
|
125
125
|
defaultSort: DEFAULT_SORT,
|
|
126
126
|
refreshing: loaded.loading && loaded.articles.length > 0,
|
|
127
127
|
error: loaded.error,
|
|
128
|
+
feedAccess: loaded.query ? loaded.newsState.access : null,
|
|
128
129
|
});
|
|
129
130
|
const { setSelectedArticleId } = stack;
|
|
130
131
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment } from "../../../../../components";
|
|
3
|
+
import { delayedFooterSegment } from "../../../../../components/layout/pane/footer/model";
|
|
4
|
+
import type { NewsAccess } from "../../../../../news/types";
|
|
3
5
|
import { t, tf } from "../../../../../i18n";
|
|
4
6
|
import { useAppLanguage } from "../../../../../i18n/react";
|
|
5
7
|
import { useCloudAccessFooter } from "../../../shared/cloud-upgrade";
|
|
@@ -22,6 +24,18 @@ interface UseNewsArticleFooterOptions {
|
|
|
22
24
|
loading?: boolean;
|
|
23
25
|
error?: string | null;
|
|
24
26
|
onPopOut?: () => void;
|
|
27
|
+
/** Which news the list on screen is, from the service's answer; null or absent when it did not say. */
|
|
28
|
+
feedAccess?: NewsAccess | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the stories are the live wire: what the service's answer said when
|
|
33
|
+
* it said; otherwise held back for an account without real-time news, and
|
|
34
|
+
* not said at all for one with it. The plan never claims the live wire.
|
|
35
|
+
*/
|
|
36
|
+
function newsFeedRealtime(feedAccess: NewsAccess | null | undefined, hasProAccess: boolean): boolean | null {
|
|
37
|
+
if (feedAccess) return feedAccess === "realtime";
|
|
38
|
+
return hasProAccess ? null : false;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
export function useNewsArticleFooter({
|
|
@@ -32,32 +46,44 @@ export function useNewsArticleFooter({
|
|
|
32
46
|
loading = false,
|
|
33
47
|
error,
|
|
34
48
|
onPopOut,
|
|
49
|
+
feedAccess = null,
|
|
35
50
|
}: UseNewsArticleFooterOptions) {
|
|
36
51
|
const language = useAppLanguage();
|
|
52
|
+
const delayLabel = tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS });
|
|
37
53
|
const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
|
|
38
|
-
delayLabel
|
|
54
|
+
delayLabel,
|
|
39
55
|
focused,
|
|
56
|
+
// A feed the service answered live has nothing to upgrade.
|
|
57
|
+
degraded: feedAccess !== "realtime",
|
|
40
58
|
segmentId: "news-access",
|
|
41
59
|
placement: "news-footer",
|
|
42
60
|
shortcutScope: `${registrationId}:news-upgrade`,
|
|
43
61
|
});
|
|
62
|
+
const realtime = newsFeedRealtime(feedAccess, access.hasProAccess);
|
|
44
63
|
|
|
45
64
|
// A report or capture of the pane says how far its stories are held back, as the footer does, and dates the
|
|
46
65
|
// list by its newest story, named so it is not read as the time now.
|
|
47
66
|
useRemoteUiNode({
|
|
48
67
|
role: "report-freshness",
|
|
49
68
|
label: "News freshness",
|
|
50
|
-
getMetadata: () => (
|
|
69
|
+
getMetadata: () => (realtime === true
|
|
51
70
|
? { status: "live", asOfLabel: NEWEST_STORY }
|
|
52
|
-
:
|
|
71
|
+
: realtime === false
|
|
72
|
+
? { status: "delayed", delayMinutes: CLOUD_NEWS_DELAY_HOURS * 60, asOfLabel: NEWEST_STORY }
|
|
73
|
+
: { status: "not-a-feed", basis: "published stories", asOfLabel: NEWEST_STORY }),
|
|
53
74
|
});
|
|
54
75
|
|
|
55
76
|
const accessInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
56
|
-
|
|
77
|
+
// A trial keeps its days-left segment, which manages the plan.
|
|
78
|
+
if (realtime === true && !access.isTrialActive) {
|
|
57
79
|
return [{ id: "news-access", parts: [{ text: t("real-time news"), tone: "positive" }] }];
|
|
58
80
|
}
|
|
81
|
+
// Held back although the account has Pro (a read that went out before its session): say so, without a pitch.
|
|
82
|
+
if (feedAccess === "delayed" && access.hasProAccess) {
|
|
83
|
+
return [delayedFooterSegment({ id: "news-access", detail: tf("{delay} delayed", { delay: delayLabel }), delay: delayLabel })];
|
|
84
|
+
}
|
|
59
85
|
return segment ? [segment] : [];
|
|
60
|
-
}, [access.
|
|
86
|
+
}, [access.hasProAccess, access.isTrialActive, delayLabel, feedAccess, language, realtime, segment]);
|
|
61
87
|
const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
|
|
62
88
|
|
|
63
89
|
// The story's header names its source, so the footer keeps to the link.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { Box } from "../../../../../ui";
|
|
3
|
-
import type { NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
3
|
+
import type { NewsAccess, NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
4
4
|
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
5
5
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
6
6
|
import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
|
|
@@ -31,6 +31,8 @@ interface NewsArticleStackOptions {
|
|
|
31
31
|
/** Rows stay on screen while a refresh runs, so the footer says it is loading. */
|
|
32
32
|
refreshing: boolean;
|
|
33
33
|
error: string | null;
|
|
34
|
+
/** Which news the rows are, from the service's answer, for the footer's real-time or delayed label. */
|
|
35
|
+
feedAccess?: NewsAccess | null;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -46,6 +48,7 @@ export function useNewsArticleStack({
|
|
|
46
48
|
defaultSort,
|
|
47
49
|
refreshing,
|
|
48
50
|
error,
|
|
51
|
+
feedAccess,
|
|
49
52
|
}: NewsArticleStackOptions) {
|
|
50
53
|
const [selectedArticleId, setSelectedArticleId] = useDebouncedPluginPaneState<string | null>(
|
|
51
54
|
`${paneKey}:selectedArticleId`,
|
|
@@ -95,6 +98,7 @@ export function useNewsArticleStack({
|
|
|
95
98
|
loading: detailLoading || refreshing,
|
|
96
99
|
error: [error, detailError].filter(Boolean).join(" ") || null,
|
|
97
100
|
onPopOut: readableArticle ? popOutReadable : undefined,
|
|
101
|
+
feedAccess,
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
return {
|
|
@@ -172,6 +176,7 @@ export function NewsPresetPane({
|
|
|
172
176
|
defaultSort,
|
|
173
177
|
refreshing: loading && persisted.length > 0,
|
|
174
178
|
error,
|
|
179
|
+
feedAccess: newsState.access,
|
|
175
180
|
});
|
|
176
181
|
const emptyCopy = newsListEmptyCopy({
|
|
177
182
|
query: searchQuery,
|
|
@@ -17,7 +17,7 @@ import { useLoadNewsStory } from "../../../../../news/hooks";
|
|
|
17
17
|
import { formatFeedTime } from "../../../../../utils/datetime-format";
|
|
18
18
|
import { logFailure } from "../../../../../utils/log-failure";
|
|
19
19
|
import { nextHeaderSort } from "../../../../../utils/sort-values";
|
|
20
|
-
import {
|
|
20
|
+
import { formatNewsCategoryForColumn } from "../categories";
|
|
21
21
|
import { useOpenTickerChoice } from "../../../shared/ticker-choice";
|
|
22
22
|
import { usePinnedMoment } from "../../../../../components/use-pinned-moment";
|
|
23
23
|
|
|
@@ -359,7 +359,7 @@ export function NewsArticleStackView({
|
|
|
359
359
|
}
|
|
360
360
|
case "categories":
|
|
361
361
|
return {
|
|
362
|
-
text:
|
|
362
|
+
text: formatNewsCategoryForColumn(item.categories[0]) || "-",
|
|
363
363
|
color: tone(colors.textDim),
|
|
364
364
|
};
|
|
365
365
|
case "sentiment": {
|
|
@@ -68,6 +68,17 @@ export function parseOpenFdaDate(value: unknown): Date | null {
|
|
|
68
68
|
|
|
69
69
|
const normalizeWords = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
70
70
|
|
|
71
|
+
/** Each term once, in the order first coded, whatever its case: `Nausea` and `NAUSEA` are one. */
|
|
72
|
+
function uniqueTerms(terms: readonly string[]): string[] {
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
return terms.filter((term) => {
|
|
75
|
+
const key = normalizeWords(term);
|
|
76
|
+
if (seen.has(key)) return false;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/** The search's words appear together in one of the fields, as openFDA matches a phrase. */
|
|
72
83
|
function fieldsMatch(fields: Array<string | undefined>, query: string): boolean {
|
|
73
84
|
const words = normalizeWords(query);
|
|
@@ -230,9 +241,10 @@ export function parseDrugReport(raw: unknown, query = "", role: DrugRole | null
|
|
|
230
241
|
product: shown?.name ?? "Unnamed drug",
|
|
231
242
|
role: shown?.role ?? null,
|
|
232
243
|
drugs: drugs.map(({ name, role }) => ({ name, role })),
|
|
233
|
-
|
|
244
|
+
// A report can code one reaction more than once (once per drug, or in another case): each is listed once.
|
|
245
|
+
reactions: uniqueTerms((Array.isArray(patient.reaction) ? patient.reaction : [])
|
|
234
246
|
.map((entry) => asString(asRecord(entry).reactionmeddrapt))
|
|
235
|
-
.filter((entry): entry is string => !!entry),
|
|
247
|
+
.filter((entry): entry is string => !!entry)),
|
|
236
248
|
outcome,
|
|
237
249
|
outcomes,
|
|
238
250
|
manufacturer: shown && shown.manufacturers.length === 1 ? shown.manufacturers[0]! : null,
|
|
@@ -3,8 +3,9 @@ import { OpenFdaPane } from "./pane";
|
|
|
3
3
|
import { OPENFDA_PANE_ID, OPENFDA_PLUGIN_ID } from "./types";
|
|
4
4
|
|
|
5
5
|
const createOpenFdaPaneInstance = (options?: PaneTemplateCreateOptions) => {
|
|
6
|
-
// A search of product and firm names
|
|
7
|
-
//
|
|
6
|
+
// A search of product and firm names: FDA records name the firm as it filed,
|
|
7
|
+
// not a symbol. A ticker that matches nothing is searched as its company's
|
|
8
|
+
// name, which the pane looks up (`FDA AZN` is AstraZeneca's reports).
|
|
8
9
|
const query = (options?.arg ?? options?.values?.query ?? "").trim();
|
|
9
10
|
const encoded = encodeURIComponent(query).replace(/%/g, "~");
|
|
10
11
|
return {
|
|
@@ -65,6 +65,15 @@ const TONES: Partial<Record<ColumnId, Record<string, StateTone>>> = {
|
|
|
65
65
|
status: { Ongoing: "warning", Completed: "text", Terminated: "muted" },
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/** The most terms a row's list cell names before counting the rest; the record names them all. */
|
|
69
|
+
const MAX_CELL_TERMS = 3;
|
|
70
|
+
|
|
71
|
+
/** A report's coded terms as a row shows them: the first few, then how many more (`Nausea, Rash, Fatigue +9 more`). */
|
|
72
|
+
export function termsCellText(terms: readonly string[]): string {
|
|
73
|
+
const more = terms.length - MAX_CELL_TERMS;
|
|
74
|
+
return `${terms.slice(0, MAX_CELL_TERMS).join(", ")}${more > 0 ? ` +${more} more` : ""}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
export function recordTone(column: ColumnId, text: string): StateTone {
|
|
69
78
|
return TONES[column]?.[text] ?? "text";
|
|
70
79
|
}
|