@gloom-sh/terminal 0.18.5 → 0.19.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- 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/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -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/core-builtin-allowlist.json +2 -2
- 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 +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- 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/saved-copy-persistence.ts +88 -0
- 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/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- 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 +8 -21
- 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/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- 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/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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 +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- 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/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -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 +17 -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/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- 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 +34 -12
- 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/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- 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/follow-binding.ts +14 -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 +5 -6
- 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/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- 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/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -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 +10 -10
- 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/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- 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/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -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 +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- 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/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- 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 +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -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 +22 -4
- 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/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -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 +6 -7
- 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/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- 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 +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- 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/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- 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 +48 -5
- 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 +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- 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/headless-market-data.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 +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- 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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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 +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- 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-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- 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 +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- 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/icons.tsx +7 -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 +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- 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/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -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 +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- 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/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- 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-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- 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/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -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 +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- 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/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -6,16 +6,20 @@ import {
|
|
|
6
6
|
validateOrganizationTickers,
|
|
7
7
|
type OrganizationTicker,
|
|
8
8
|
} from "../../../api-client/organization-tickers";
|
|
9
|
+
import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
|
|
10
|
+
import { findSponsorListings, type ListingFinder } from "./sponsor-listing";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* A lead sponsor's ticker,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* A lead sponsor's ticker, as Gloom Cloud resolved it, or, for a company it
|
|
14
|
+
* has no reviewed name for, as the listing search finds it (`listingSearch`).
|
|
15
|
+
* `none` is an answer (no listed company, or a name that fits several);
|
|
16
|
+
* `failed` means it was not looked up at all and says nothing about the
|
|
17
|
+
* sponsor.
|
|
14
18
|
*/
|
|
15
19
|
export type SponsorTicker =
|
|
16
20
|
| { kind: "pending" }
|
|
17
21
|
| { kind: "listed"; ticker: string; exchange: string | null; entityName: string | null }
|
|
18
|
-
| { kind: "none"; entityName: string | null; ambiguous: boolean }
|
|
22
|
+
| { kind: "none"; entityName: string | null; ambiguous: boolean; listingSearch?: "done" }
|
|
19
23
|
| { kind: "failed"; reason: string };
|
|
20
24
|
|
|
21
25
|
/** A ticker only when one company and its listing were found; an ambiguous name never gets one. */
|
|
@@ -46,17 +50,21 @@ const sponsorKey = (name: string) => name.trim().replace(/\s+/g, " ").toLowerCas
|
|
|
46
50
|
* never stored as having no ticker because the lookup did not happen.
|
|
47
51
|
*/
|
|
48
52
|
export class SponsorTickerStore {
|
|
49
|
-
private readonly answers
|
|
53
|
+
private readonly answers: TtlCache<SponsorTicker>;
|
|
50
54
|
private readonly failures = new Map<string, string>();
|
|
51
55
|
private readonly inFlight = new Set<string>();
|
|
56
|
+
private readonly searching = new Set<string>();
|
|
57
|
+
private readonly searchFailures = new Set<string>();
|
|
52
58
|
private readonly listeners = new Set<() => void>();
|
|
53
59
|
|
|
54
|
-
constructor(private readonly lookup: Lookup,
|
|
60
|
+
constructor(private readonly lookup: Lookup, now: () => number = Date.now) {
|
|
61
|
+
this.answers = createTtlCache<SponsorTicker>({ ttlMs: CACHE_TTL_MS, now });
|
|
62
|
+
}
|
|
55
63
|
|
|
56
64
|
get(name: string): SponsorTicker {
|
|
57
65
|
const key = sponsorKey(name);
|
|
58
|
-
const answer = this.answers.
|
|
59
|
-
if (answer
|
|
66
|
+
const answer = this.answers.peek(key);
|
|
67
|
+
if (answer) return answer;
|
|
60
68
|
const reason = this.failures.get(key);
|
|
61
69
|
return reason ? { kind: "failed", reason } : { kind: "pending" };
|
|
62
70
|
}
|
|
@@ -86,8 +94,7 @@ export class SponsorTickerStore {
|
|
|
86
94
|
keys.forEach((key) => this.inFlight.add(key));
|
|
87
95
|
try {
|
|
88
96
|
const results = await this.lookup(batch, signal);
|
|
89
|
-
|
|
90
|
-
results.forEach((result, index) => this.answers.set(keys[index]!, { value: sponsorTickerFrom(result), expiresAt }));
|
|
97
|
+
results.forEach((result, index) => this.answers.set(keys[index]!, sponsorTickerFrom(result)));
|
|
91
98
|
} catch (error) {
|
|
92
99
|
// An abandoned lookup (its pane closed) is neither an answer nor a failure.
|
|
93
100
|
if (!signal?.aborted) {
|
|
@@ -102,24 +109,94 @@ export class SponsorTickerStore {
|
|
|
102
109
|
}));
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
/**
|
|
113
|
+
* `load`, then `searchListings` for the companies among the names, but the
|
|
114
|
+
* caller waits no longer than `ms`: the lookups go on and tickers fill in
|
|
115
|
+
* when they answer.
|
|
116
|
+
*/
|
|
117
|
+
async loadWithin(
|
|
118
|
+
names: readonly string[],
|
|
119
|
+
ms: number,
|
|
120
|
+
signal?: AbortSignal,
|
|
121
|
+
listings?: { companies: readonly string[]; find?: ListingFinder },
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
124
|
+
const patience = new Promise<void>((resolve) => { timer = setTimeout(resolve, ms); });
|
|
125
|
+
const lookups = this.load(names, signal).then(() => listings ? this.searchListings(listings.companies, listings.find, signal) : undefined);
|
|
126
|
+
try {
|
|
127
|
+
await Promise.race([lookups, patience]);
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Looks the companies Gloom Cloud had no name for up in Gloom Cloud's
|
|
135
|
+
* search, the one that finds `Bayer` when it is typed, and keeps a primary
|
|
136
|
+
* listing as the sponsor's ticker (`findSponsorListings` says which names
|
|
137
|
+
* qualify). Only names Cloud answered with no company, no listing and no
|
|
138
|
+
* ambiguity are asked: a company it knows, and a name that fits several,
|
|
139
|
+
* stay as it said. Each name is searched once, all in one request; a search
|
|
140
|
+
* that failed waits for `retryFailed`, and tells nothing about the sponsor.
|
|
141
|
+
*/
|
|
142
|
+
async searchListings(names: readonly string[], find: ListingFinder = findSponsorListings, signal?: AbortSignal): Promise<void> {
|
|
143
|
+
const asked = new Map<string, string>();
|
|
144
|
+
for (const name of names) {
|
|
145
|
+
const trimmed = name.trim();
|
|
146
|
+
const key = sponsorKey(trimmed);
|
|
147
|
+
const answer = this.answers.peek(key);
|
|
148
|
+
if (!trimmed || asked.has(key) || this.searching.has(key) || this.searchFailures.has(key)) continue;
|
|
149
|
+
if (answer?.kind !== "none" || answer.ambiguous || answer.entityName || answer.listingSearch) continue;
|
|
150
|
+
asked.set(key, trimmed);
|
|
151
|
+
}
|
|
152
|
+
if (asked.size === 0) return;
|
|
153
|
+
const keys = [...asked.keys()];
|
|
154
|
+
keys.forEach((key) => this.searching.add(key));
|
|
155
|
+
try {
|
|
156
|
+
const found = await find([...asked.values()], signal);
|
|
157
|
+
if (signal?.aborted) return;
|
|
158
|
+
let changed = false;
|
|
159
|
+
for (const [key, name] of asked) {
|
|
160
|
+
const listing = found.get(name) ?? null;
|
|
161
|
+
this.answers.set(key, listing
|
|
162
|
+
? { kind: "listed", ticker: listing.ticker, exchange: listing.exchange, entityName: listing.entityName }
|
|
163
|
+
: { kind: "none", entityName: null, ambiguous: false, listingSearch: "done" });
|
|
164
|
+
changed ||= listing !== null;
|
|
165
|
+
}
|
|
166
|
+
if (changed) this.listeners.forEach((listener) => listener());
|
|
167
|
+
} catch {
|
|
168
|
+
// A failed search says nothing about the sponsor; it is asked again on retry.
|
|
169
|
+
if (!signal?.aborted) keys.forEach((key) => this.searchFailures.add(key));
|
|
170
|
+
} finally {
|
|
171
|
+
keys.forEach((key) => this.searching.delete(key));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
105
175
|
/** Forgets failed lookups, so the next `load` asks again. */
|
|
106
176
|
retryFailed(): void {
|
|
107
|
-
if (this.failures.size === 0) return;
|
|
177
|
+
if (this.failures.size === 0 && this.searchFailures.size === 0) return;
|
|
108
178
|
this.failures.clear();
|
|
179
|
+
this.searchFailures.clear();
|
|
109
180
|
this.listeners.forEach((listener) => listener());
|
|
110
181
|
}
|
|
111
182
|
}
|
|
112
183
|
|
|
113
|
-
const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
|
|
184
|
+
export const sponsorTickerStore = new SponsorTickerStore(async (names, signal) => (
|
|
114
185
|
validateOrganizationTickers(await apiClient.getCloudOrganizationTickers(names, signal), names)
|
|
115
186
|
));
|
|
116
187
|
|
|
188
|
+
export interface SponsorTickersOptions {
|
|
189
|
+
/** The company sponsors among `names`: the ones Cloud's search may look up when its ticker lookup has no name for them. */
|
|
190
|
+
companies?: readonly string[];
|
|
191
|
+
store?: SponsorTickerStore;
|
|
192
|
+
}
|
|
193
|
+
|
|
117
194
|
/**
|
|
118
195
|
* The tickers for the sponsors on screen. Rows draw at once and tickers fill
|
|
119
196
|
* in when the lookup answers; `retry` asks again for the ones that failed.
|
|
120
197
|
* `sessionKey` changing (a sign-in) retries them too.
|
|
121
198
|
*/
|
|
122
|
-
export function useSponsorTickers(names: readonly string[], sessionKey: string, store:
|
|
199
|
+
export function useSponsorTickers(names: readonly string[], sessionKey: string, { companies, store = sponsorTickerStore }: SponsorTickersOptions = {}) {
|
|
123
200
|
const [version, setVersion] = useState(0);
|
|
124
201
|
const [epoch, setEpoch] = useState(0);
|
|
125
202
|
const [signal, setSignal] = useState<AbortSignal | null>(null);
|
|
@@ -134,10 +211,13 @@ export function useSponsorTickers(names: readonly string[], sessionKey: string,
|
|
|
134
211
|
store.retryFailed();
|
|
135
212
|
}, [sessionKey, store]);
|
|
136
213
|
const namesKey = names.join("\n");
|
|
214
|
+
const companiesKey = (companies ?? []).join("\n");
|
|
137
215
|
useEffect(() => {
|
|
138
216
|
if (!signal || signal.aborted || !namesKey) return;
|
|
139
|
-
void store.load(namesKey.split("\n"), signal)
|
|
140
|
-
|
|
217
|
+
void store.load(namesKey.split("\n"), signal).then(() => {
|
|
218
|
+
if (companiesKey) return store.searchListings(companiesKey.split("\n"), findSponsorListings, signal);
|
|
219
|
+
});
|
|
220
|
+
}, [companiesKey, epoch, namesKey, sessionKey, signal, store, version]);
|
|
141
221
|
const retry = useCallback(() => {
|
|
142
222
|
store.retryFailed();
|
|
143
223
|
setEpoch((current) => current + 1);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PageRequest, RowPage } from "../../../components";
|
|
2
|
+
import type { ClinicalTrial, ClinicalTrialsPage, StudiesQuery } from "./client";
|
|
3
|
+
|
|
4
|
+
/** What one stage of the list asks the registry for on top of the search: a sponsor class to keep or to leave out. */
|
|
5
|
+
export type TrialStage = Pick<StudiesQuery, "sponsorClasses" | "excludedSponsorClasses">;
|
|
6
|
+
|
|
7
|
+
/** The companies' studies first and everything else after them. */
|
|
8
|
+
export const COMPANIES_FIRST: readonly TrialStage[] = [{ sponsorClasses: ["INDUSTRY"] }, { excludedSponsorClasses: ["INDUSTRY"] }];
|
|
9
|
+
|
|
10
|
+
/** Where a page continues: which stage it is in, and the registry's token inside it. */
|
|
11
|
+
interface StageCursor {
|
|
12
|
+
stage: number;
|
|
13
|
+
token?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pages the registry through `stages` one after the other: a stage's studies
|
|
18
|
+
* come newest first, and when it has no more the next stage starts, so the
|
|
19
|
+
* list reaches a university's study only once the companies' are in. The
|
|
20
|
+
* registry pages by an opaque token, which the next page's offset keys; the
|
|
21
|
+
* first page always starts at the first stage, so a reload starts over. A
|
|
22
|
+
* stage with nothing in it goes straight on to the next, so an empty page is
|
|
23
|
+
* never the end of the list while a stage is left. `onPage` runs on a page's
|
|
24
|
+
* studies before it is handed over.
|
|
25
|
+
*/
|
|
26
|
+
export function createTrialPageLoader({ list, query, stages, onPage }: {
|
|
27
|
+
list: (query: StudiesQuery, signal: AbortSignal) => Promise<ClinicalTrialsPage>;
|
|
28
|
+
query: StudiesQuery;
|
|
29
|
+
stages: readonly TrialStage[];
|
|
30
|
+
onPage?: (trials: ClinicalTrial[], signal: AbortSignal) => Promise<void>;
|
|
31
|
+
}): (request: Pick<PageRequest, "offset" | "signal">) => Promise<RowPage<ClinicalTrial>> {
|
|
32
|
+
const cursors = new Map<number, StageCursor>();
|
|
33
|
+
return async ({ offset, signal }) => {
|
|
34
|
+
let cursor: StageCursor | undefined = offset === 0 ? { stage: 0 } : cursors.get(offset);
|
|
35
|
+
while (cursor) {
|
|
36
|
+
const page: ClinicalTrialsPage = await list({ ...query, ...stages[cursor.stage], pageToken: cursor.token }, signal);
|
|
37
|
+
const next: StageCursor | null = page.nextPageToken ? { stage: cursor.stage, token: page.nextPageToken }
|
|
38
|
+
: cursor.stage + 1 < stages.length ? { stage: cursor.stage + 1 } : null;
|
|
39
|
+
if (page.trials.length === 0 && next && !page.nextPageToken) {
|
|
40
|
+
cursor = next;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const nextOffset = offset + page.trials.length;
|
|
44
|
+
if (next) cursors.set(nextOffset, next);
|
|
45
|
+
await onPage?.(page.trials, signal);
|
|
46
|
+
return { rows: page.trials, hasMore: !!next, nextOffset };
|
|
47
|
+
}
|
|
48
|
+
return { rows: [], hasMore: false };
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -142,6 +142,8 @@ function headlessCandidates(registry: PaneFunctionCatalog): ManifestCandidate[]
|
|
|
142
142
|
const pane = registry.panes.get(template.paneId);
|
|
143
143
|
if (!pane) continue;
|
|
144
144
|
templatedPaneIds.add(pane.id);
|
|
145
|
+
// An unlisted function (`listed: false`) is no tool, and its pane none in its place.
|
|
146
|
+
if (template.listed === false) continue;
|
|
145
147
|
const definition = definitionFor(template, pane);
|
|
146
148
|
if (!definition) continue;
|
|
147
149
|
const token = template.shortcut?.prefix ?? template.id;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
1
2
|
import { useCallback, useMemo, useRef, type ReactNode } from "react";
|
|
2
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
4
|
import type { HiringBuildingFilter, HiringBuildingShift } from "../../../api-client/hiring";
|
|
@@ -60,7 +61,10 @@ export function HiringBuildingView({ symbol, width, height, focused, accessKey,
|
|
|
60
61
|
try {
|
|
61
62
|
const payload = snapshotSetting ? validateBuilding(snapshotSetting) : null;
|
|
62
63
|
return payload && ("filter" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
|
|
63
|
-
} catch {
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logFailure("company-attention.building", "Validate hiring building snapshot failed")(error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
64
68
|
}, [snapshotSetting, symbol]);
|
|
65
69
|
const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadBuilding(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
|
|
66
70
|
const resource = useAsyncResource(loader, { initialData: () => cachedBuilding(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
|
|
@@ -3,6 +3,7 @@ import type { AppAttentionFilter, AppAttentionPayload, AppRankPayload } from "..
|
|
|
3
3
|
import type { HiringBoard, HiringBuildingBoard, HiringBuildingFilter, HiringBuildingShift, HiringCompanyBuilding, HiringCompanyMoves, HiringCompanySignals, HiringMixRow, HiringMove, HiringMovePosting, HiringMovesBoard, HiringMovesFilter, HiringMoveWeek, HiringPayload, HiringSignalGroup, HiringSignalsBoard, HiringSignalsFilter, HiringSummary, HiringWeekSignal } from "../../../api-client/hiring";
|
|
4
4
|
import type { CloudJobsPostingDetail } from "../../../api-client/types";
|
|
5
5
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
6
|
+
import { createTtlCache } from "../../../data/ttl-cache";
|
|
6
7
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
7
8
|
import type { AppFocus, AttentionKind } from "./model";
|
|
8
9
|
|
|
@@ -19,7 +20,10 @@ const finite = (value: unknown) => typeof value === "number" && Number.isFinite(
|
|
|
19
20
|
const positive = (value: unknown) => finite(value) && (value as number) >= 0;
|
|
20
21
|
const fraction = (value: unknown) => positive(value) && (value as number) <= 1;
|
|
21
22
|
const date = (value: unknown) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
22
|
-
const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch {
|
|
23
|
+
const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch {
|
|
24
|
+
// Intentional ignore: malformed URLs fail this web link field validator.
|
|
25
|
+
return false;
|
|
26
|
+
} };
|
|
23
27
|
function numericTree(value: unknown): boolean {
|
|
24
28
|
if (typeof value === "number") return Number.isFinite(value);
|
|
25
29
|
if (Array.isArray(value)) return value.every(numericTree);
|
|
@@ -165,21 +169,16 @@ export async function fetchAppRank(focus: AppFocus, days: number, offset = 0, si
|
|
|
165
169
|
export const loadAppRank = (focus: AppFocus, access: string, days: number, force = false) => loadCloudResource(appRankCache, `${access}:${focus.store}:${focus.appId}:${focus.country}:${focus.chart}:${days}`, () => fetchAppRank(focus, days), { force });
|
|
166
170
|
|
|
167
171
|
const POSTING_TTL_MS = 5 * 60_000;
|
|
168
|
-
const
|
|
172
|
+
const postings = createTtlCache<CloudJobsPostingDetail>({ ttlMs: POSTING_TTL_MS });
|
|
169
173
|
/**
|
|
170
174
|
* The page HIRE's evidence row was read from: its title, place, date, pay,
|
|
171
175
|
* hours and description. Kept a few minutes, so stepping back and forth
|
|
172
176
|
* through the evidence does not read it again.
|
|
173
177
|
*/
|
|
174
178
|
export function fetchPostingDetail(ticker: string, externalId: string, client: Pick<typeof apiClient, "getCloudJobsPosting"> = apiClient): Promise<CloudJobsPostingDetail> {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (cached && Date.now() - cached.at < POSTING_TTL_MS) return cached.posting;
|
|
178
|
-
const posting = client.getCloudJobsPosting(ticker, { externalId }).then((detail) => {
|
|
179
|
+
return postings.load(`${ticker.toUpperCase()}:${externalId}`, async () => {
|
|
180
|
+
const detail = await client.getCloudJobsPosting(ticker, { externalId });
|
|
179
181
|
if (!detail || typeof detail.title !== "string" || detail.externalId !== externalId) throw new Error("The server returned an unreadable posting.");
|
|
180
182
|
return detail;
|
|
181
183
|
});
|
|
182
|
-
postingCache.set(key, { at: Date.now(), posting });
|
|
183
|
-
posting.catch(() => postingCache.delete(key));
|
|
184
|
-
return posting;
|
|
185
184
|
}
|
|
@@ -45,7 +45,10 @@ export function attentionScreenshotEvidence(kind: AttentionKind): PaneScreenshot
|
|
|
45
45
|
read(value) {
|
|
46
46
|
if (!isRecord(value) || value.kind !== kind || value.version !== 1 || value.complete !== true || !isRecord(value.payload)
|
|
47
47
|
|| !["table", "chart", "mix", "peers", "evidence", ...(kind === "hiring" ? ["moves", "expansion", "anomalies", "building"] : [])].includes(String(value.tab)) || !Array.isArray(value.rowIds) || !value.rowIds.every((id) => typeof id === "string") || value.plottedValueCount !== value.rowIds.length) return null;
|
|
48
|
-
try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch {
|
|
48
|
+
try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch {
|
|
49
|
+
// Intentional ignore: invalid captured observations are rejected as evidence by returning null.
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
49
52
|
},
|
|
50
53
|
mismatches(evidence, { resolved, payload }) {
|
|
51
54
|
const settings = payload.config.layout.instances.find((entry) => entry.instanceId === payload.paneId)?.settings;
|
|
@@ -3,6 +3,7 @@ import type { DataTableColumn } from "../../../components";
|
|
|
3
3
|
import type { AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
|
|
4
4
|
import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
|
|
5
5
|
import { fetchAppRank, fetchAttention, fetchBuilding, fetchMoves, fetchSignals, type BuildingPayload, type SignalsPayload } from "./client";
|
|
6
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
6
7
|
import { BUILDING_FILTERS, BUILDING_VIEWS, buildingBoardModel, buildingNumber, buildingPreviewNote, companyBuildingModel, isBuildingFilter, type BuildingSection } from "./building-model";
|
|
7
8
|
import { appsModel, appsPreviewNote, attentionNumber, attentionSectionTitle, attentionTabs, hiringModel, hiringPreviewNote, remoteText, type AttentionKind } from "./model";
|
|
8
9
|
import { companyMovesModel, isMovesFilter, movesBoardModel, movesNumber, movesPreviewNote, MOVES_FILTERS, MOVES_VIEWS, type MovesSection } from "./moves-model";
|
|
@@ -146,7 +147,7 @@ export function attentionHeadless(kind: AttentionKind): HeadlessPaneDefinition<"
|
|
|
146
147
|
const why = movesModel?.latest?.why;
|
|
147
148
|
const company = args.symbols[0] ? signalsReads[0]?.data : undefined;
|
|
148
149
|
const held = company && !("group" in company) && company.held ? holdText(company.held) : null;
|
|
149
|
-
const notes = [...model.notices, ...(nextOffset !== null ? [`Additional observations available with --offset ${nextOffset}.`] : []),
|
|
150
|
+
const notes = [...model.notices, ...companyDataNotes("companyFrom" in data ? data.companyFrom : null), ...(nextOffset !== null ? [`Additional observations available with --offset ${nextOffset}.`] : []),
|
|
150
151
|
...(movesData instanceof Error ? [movesData.message] : []), ...(why ? [why] : []),
|
|
151
152
|
...(signalsData instanceof Error ? [signalsData.message] : []), ...(held ? [`No expansion or anomaly signal this week: ${held.toLowerCase()}.`] : []),
|
|
152
153
|
...(buildingData instanceof Error ? [buildingData.message] : []), ...(buildingHeld ? [`No mix to compare this week: ${buildingHeld.toLowerCase()}.`] : [])];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
1
2
|
import { useCallback, useMemo, useRef, type ReactNode } from "react";
|
|
2
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
4
|
import type { HiringMovesFilter } from "../../../api-client/hiring";
|
|
@@ -63,7 +64,10 @@ export function HiringMovesView({ symbol, width, height, focused, accessKey, com
|
|
|
63
64
|
try {
|
|
64
65
|
const payload = snapshotSetting ? validateMoves(snapshotSetting) : null;
|
|
65
66
|
return payload && ("moves" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
|
|
66
|
-
} catch {
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logFailure("company-attention.moves", "Validate hiring moves snapshot failed")(error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
67
71
|
}, [snapshotSetting, symbol]);
|
|
68
72
|
const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadMoves(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
|
|
69
73
|
const resource = useAsyncResource(loader, { initialData: () => cachedMoves(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
1
2
|
import { useCallback, useMemo, useRef } from "react";
|
|
2
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
4
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
5
|
import type { AppAttentionFilter, AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
|
|
5
6
|
import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
|
|
6
7
|
import { ChartTableHeader, DataTableView, PageStackView, PaneFooterScope, PaneLinkMenu, QueryBar, useChartTableSelection, usePagedRows, useTableLoadMore, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type PaneHint } from "../../../components";
|
|
8
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
7
9
|
import { DetailReader, type DetailFact } from "../../../components/reader/detail-reader";
|
|
8
10
|
import type { CloudJobsPostingDetail } from "../../../api-client/types";
|
|
9
11
|
import { descriptionText, postingFacts, postingTitle } from "../shared/job-posting";
|
|
@@ -57,7 +59,10 @@ function Evidence({ row, width, posting, source }: Size & { row: AttentionRow; p
|
|
|
57
59
|
}
|
|
58
60
|
/** A link's site, as a reader names it: "sec.gov". */
|
|
59
61
|
function linkName(url: string): string {
|
|
60
|
-
try { return new URL(url).hostname.replace(/^www\./, ""); } catch {
|
|
62
|
+
try { return new URL(url).hostname.replace(/^www\./, ""); } catch {
|
|
63
|
+
// Intentional ignore: a malformed link has no site to name, so it reads as Source.
|
|
64
|
+
return "Source";
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export function HiringPane(props: Size) {
|
|
@@ -105,7 +110,10 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
|
|
|
105
110
|
const [mix, setMix] = usePluginPaneState<string>(`${kind}:mix`, kind === "hiring" ? "functions" : "countries");
|
|
106
111
|
const filters = useMemo<AppAttentionFilter>(() => kind === "apps" ? { ...(country ? { country } : {}), ...(chart ? { chart: chart as AppChart } : {}), days: Number(days) || 90 } : {}, [kind, country, chart, days]);
|
|
107
112
|
const [snapshotSetting] = usePaneSettingValue<AttentionPayload | null>("attentionSnapshot", null);
|
|
108
|
-
const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch {
|
|
113
|
+
const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch (error) {
|
|
114
|
+
logFailure("company-attention.pane", "Validate attention snapshot failed")(error);
|
|
115
|
+
return null;
|
|
116
|
+
} }, [kind, snapshotSetting]);
|
|
109
117
|
const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadAttention(kind, symbol, accessKey, filters, force), [kind, symbol, accessKey, filters, snapshot]);
|
|
110
118
|
const resource = useAsyncResource(loader, { initialData: () => cachedAttention(kind, symbol, accessKey, filters), clearOnError: isAccessDenied });
|
|
111
119
|
const data = resource.data?.payload;
|
|
@@ -177,6 +185,8 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
|
|
|
177
185
|
usePaneStatusFooter({ registrationId: `${kind}:status`, enabled: !openCompany && !app && !moves, loading: resource.loading || paged.loadingMore || (!!evidence && postingResource.loading), error: data ? resource.error ?? paged.moreError?.message ?? paged.error?.message : null, stale: resource.data?.stale,
|
|
178
186
|
info: [...(model?.asOf ? [{ id: "observed", asOf: true, parts: [{ text: `as of ${model.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []), ...(model?.preview ? [{ id: "preview", parts: [{ text: "Pro preview", tone: "warning" as const }] }] : [])], hints });
|
|
179
187
|
usePaneNoticeFooter({ registrationId: `${kind}:notices`, enabled: !openCompany && !app && !moves, focused, notices: [...(model?.notices ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
|
|
188
|
+
// A receipt's or foreign line's hiring or apps are its company's, read from the home line.
|
|
189
|
+
useCompanyDataFooter(data && "companyFrom" in data ? data.companyFrom : null, !openCompany && !app && !moves);
|
|
180
190
|
if (signInWall) return <SignInWall placement={kind === "hiring" ? "hiring-momentum-signin" : "app-attention-signin"} action={kind === "hiring" ? "view hiring momentum" : "view app attention"} needsVerification={session.needsVerification} />;
|
|
181
191
|
const countryOptions = data && kind === "apps" ? (data as AppAttentionPayload).coverage.configuredCountries : [];
|
|
182
192
|
const query = kind === "hiring" && tab !== "mix" ? undefined : <QueryBar width={width} filters={kind === "apps" ? [
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
1
2
|
import { useCallback, useMemo, useRef, type ReactNode } from "react";
|
|
2
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
4
|
import type { HiringSignalGroup, HiringSignalsFilter, HiringWeekSignal } from "../../../api-client/hiring";
|
|
@@ -60,7 +61,10 @@ export function HiringSignalsView({ group, symbol, width, height, focused, acces
|
|
|
60
61
|
try {
|
|
61
62
|
const payload = snapshotSetting ? validateSignals(snapshotSetting) : null;
|
|
62
63
|
return payload && ("group" in payload ? !symbol && payload.group === group : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
|
|
63
|
-
} catch {
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logFailure("company-attention.signals", "Validate hiring signals snapshot failed")(error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
64
68
|
}, [snapshotSetting, symbol, group]);
|
|
65
69
|
const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadSignals(group, symbol, accessKey, filter, force), [snapshot, group, symbol, accessKey, filter]);
|
|
66
70
|
const resource = useAsyncResource(loader, { initialData: () => cachedSignals(group, symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { DataTableColumn } from "../../../components";
|
|
2
2
|
import { DATE_OPTION_EXAMPLES, isoDateOption } from "../../../utils/calendar-date";
|
|
3
3
|
import { optionFlag } from "../../../cli/pane-functions/capabilities";
|
|
4
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
4
5
|
import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
5
6
|
import { withoutQuietColumns } from "../shared/research-cells";
|
|
6
7
|
import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
7
8
|
import { companyQuery, fetchCompanyData, type CompanyDataset, type CompanyMode } from "./client";
|
|
8
|
-
import { allGuidance, allObservations, companyCellText, companyColumns, companyPreviewNote, KPI_COVERAGE_NOTE, kpiFreeLimitNote, QUIET_COLUMNS,
|
|
9
|
+
import { allGuidance, allObservations, companyCellText, companyColumns, companyEmptyState, companyPreviewNote, KPI_COVERAGE_NOTE, kpiFreeLimitNote, QUIET_COLUMNS,
|
|
9
10
|
type CompanyRow } from "./model";
|
|
10
11
|
import type { KpiObservation } from "../../../api-client/company-kpis";
|
|
11
12
|
|
|
@@ -107,8 +108,12 @@ export function companyHeadless(mode: CompanyMode): HeadlessPaneDefinition<"bund
|
|
|
107
108
|
const latest = ("series" in data ? data.series.map((series) => series.latest) : data.guidance).filter(keep);
|
|
108
109
|
const ignored = data.access === "preview" ? PRO_FILTERS.filter((key) => typeof args.options[key] === "string" && args.options[key] !== "") : [];
|
|
109
110
|
const notes = [ageNote(data, latest, mode),
|
|
110
|
-
ignored.length ? `The free preview is fixed: ${ignored.map(optionFlag).join(", ")} ${ignored.length > 1 ? "need" : "needs"} Gloom Pro.` : null
|
|
111
|
-
|
|
111
|
+
ignored.length ? `The free preview is fixed: ${ignored.map(optionFlag).join(", ")} ${ignored.length > 1 ? "need" : "needs"} Gloom Pro.` : null,
|
|
112
|
+
...companyDataNotes(data.companyFrom)].filter((note): note is string => !!note);
|
|
113
|
+
// A company with nothing on file says so once, as the pane does, rather than "not covered".
|
|
114
|
+
const nothingOnFile = datasetRows(data).length === 0 && data.lockedRows === 0 && !requested;
|
|
115
|
+
const { title, hint } = companyEmptyState(mode, symbol, { tab: "table", hasRows: !nothingOnFile }, "cli");
|
|
116
|
+
return { complete: !data.truncated, ...(nothingOnFile ? { empty: `${title} ${hint}` } : {}), errors: data.truncated ? [data.access === "preview" ? companyPreviewNote(data, mode === "guidance") : "Response limit reached; narrow the metric or date range"] : [],
|
|
112
117
|
...(notes.length ? { notes } : {}),
|
|
113
118
|
sections: "series" in data ? [
|
|
114
119
|
section("Latest KPIs", data.series.map((series) => series.latest), { first: true }),
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { GuidancePayload, KpiGuidance, KpiObservation, KpiSeries, KpiUnit, KpisPayload } from "../../../api-client/company-kpis";
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
3
|
import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
|
|
4
|
+
import { getTableWidth, tableColumnWidth } from "../../../components/ui/table-layout";
|
|
4
5
|
import type { CompositeChartXAxis } from "../../../components/chart/composite/types";
|
|
5
6
|
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
6
7
|
import { freePreviewNote } from "../shared/free-preview";
|
|
8
|
+
import type { CompanyMode } from "./client";
|
|
7
9
|
|
|
8
10
|
export type CompanyRow = KpiObservation | KpiGuidance;
|
|
9
11
|
export const isGuidance = (row: CompanyRow): row is KpiGuidance => "issuedDate" in row;
|
|
@@ -89,13 +91,30 @@ function unitSuffix(row: Pick<CompanyRow, "unit" | "currency" | "dimensions">):
|
|
|
89
91
|
if (row.unit === "currency_per_unit" || row.unit === "volume") return ` ${unitLabel(row)}`;
|
|
90
92
|
return "";
|
|
91
93
|
}
|
|
92
|
-
/** Raised reads as good news, a cut as bad, a reiteration as no news. */
|
|
93
|
-
export const directionTone = (
|
|
94
|
-
: direction === "withdrawn" ? "warning" as const :
|
|
94
|
+
/** Raised reads as good news, a cut as bad, a reiteration as no news, and a New guide as itself whichever way the service reached it. */
|
|
95
|
+
export const directionTone = (guide: Pick<KpiGuidance, "direction" | "previousId">) => guide.direction === "raised" ? "positive" as const : guide.direction === "cut" ? "negative" as const
|
|
96
|
+
: guide.direction === "withdrawn" ? "warning" as const : directionLabel(guide) === "New" ? "text" as const : "muted" as const;
|
|
95
97
|
/** A change is good when it moves the way the metric's owner wants it to. */
|
|
96
98
|
export const changeTone = (row: KpiObservation, value: number | null) => value === null || value === 0 || row.metric.favorable === "neutral" ? "muted" as const
|
|
97
99
|
: (value > 0) === (row.metric.favorable === "higher") ? "positive" as const : "negative" as const;
|
|
98
|
-
|
|
100
|
+
/**
|
|
101
|
+
* What a guide did against the one before it for the same period, in plain
|
|
102
|
+
* words. A guide with no earlier one to set it against is New, whether it is
|
|
103
|
+
* the company's first or the first for its period. The service sets a guide it
|
|
104
|
+
* cannot rank (a different hedge or conditions) beside its predecessor as not
|
|
105
|
+
* comparable: that one Changed, with no word for up or down.
|
|
106
|
+
*/
|
|
107
|
+
export function directionLabel(guide: Pick<KpiGuidance, "direction" | "previousId">): string {
|
|
108
|
+
switch (guide.direction) {
|
|
109
|
+
case "raised": return "Raised";
|
|
110
|
+
case "cut": return "Lowered";
|
|
111
|
+
case "reiterated": return "Maintained";
|
|
112
|
+
case "mixed": return "Mixed";
|
|
113
|
+
case "withdrawn": return "Withdrawn";
|
|
114
|
+
case "initiated": return "New";
|
|
115
|
+
case "not_comparable": return guide.previousId ? "Changed" : "New";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
99
118
|
export const outcomeLabel = (row: KpiGuidance) => !row.actual ? "Pending" : row.actual.favorable === "in_line" ? "In range"
|
|
100
119
|
: row.actual.favorable === "neutral" ? row.actual.outcome === "above" ? "Above" : row.actual.outcome === "below" ? "Below" : "In range"
|
|
101
120
|
: row.actual.favorable === "beat" ? "Beat" : "Miss";
|
|
@@ -163,7 +182,7 @@ export const QUIET_COLUMNS = { basis: ["Reported"], scope: ["Consolidated"] };
|
|
|
163
182
|
/** The text of a table cell as the pane draws it; the report prints the same words. */
|
|
164
183
|
export function companyCellText(row: CompanyRow, column: string, observations: KpiObservation[]): string {
|
|
165
184
|
const guide = isGuidance(row) ? row : null;
|
|
166
|
-
if (column === "change") return guide ? directionLabel(guide
|
|
185
|
+
if (column === "change") return guide ? directionLabel(guide) : changeText(observationChange(row as KpiObservation, observations));
|
|
167
186
|
return column === "metric" ? row.metric.name : column === "period" ? fiscalLabel(row) : column === "value" ? `${guide ? rangeText(guide) : observationValue(row as KpiObservation)}${guide?.status === "withdrawn" || guide?.hedge === "qualitative" ? "" : unitSuffix(row)}`
|
|
168
187
|
: column === "unit" ? unitLabel(row)
|
|
169
188
|
: column === "basis" ? basisLabel(row.basis) : column === "scope" ? dimensionLabel(row.dimensions) || "Consolidated"
|
|
@@ -195,6 +214,55 @@ export function companyColumns(guidance: boolean, evidence: boolean, history: bo
|
|
|
195
214
|
return order ? shown.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)) : shown;
|
|
196
215
|
}
|
|
197
216
|
|
|
217
|
+
/** Cells a column gives up on a narrow pane, before any column is dropped: a date needs 10, a range about 19. */
|
|
218
|
+
const NARROW_WIDTHS: Record<string, number> = { value: 19, change: 11, issued: 10, actual: 12, outcome: 9, basis: 12, scope: 18 };
|
|
219
|
+
/** The metric keeps at least this much: a name such as "Revenue growth", and its series detail only when there is room. */
|
|
220
|
+
const METRIC_FLOOR = 14;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The table's columns at a pane `width` in cells, so none is cut at the right edge.
|
|
224
|
+
* A table that already fits is left as it is. Otherwise the columns take their
|
|
225
|
+
* narrow widths, and the metric, which flexes, keeps what is left down to its
|
|
226
|
+
* floor. Below that a column whose meaning is in the detail goes (Action in History,
|
|
227
|
+
* where the outcome says it, then Issued), never one that names the row. A table that
|
|
228
|
+
* cannot fit even so keeps its narrow widths and scrolls sideways, as it always has.
|
|
229
|
+
*/
|
|
230
|
+
export function fitCompanyColumns(columns: readonly DataTableColumn[], width: number, history = false): DataTableColumn[] {
|
|
231
|
+
const room = width - 1;
|
|
232
|
+
if (getTableWidth(columns) <= room) return [...columns];
|
|
233
|
+
const narrow = (list: readonly DataTableColumn[]) => list.map((column) => column.id in NARROW_WIDTHS ? { ...column, width: Math.min(column.width, NARROW_WIDTHS[column.id]!) } : column);
|
|
234
|
+
const attempt = (list: readonly DataTableColumn[]): DataTableColumn[] | null => {
|
|
235
|
+
const tight = narrow(list);
|
|
236
|
+
const metric = tight.find((column) => column.id === "metric");
|
|
237
|
+
if (!metric) return getTableWidth(tight) <= room ? tight : null;
|
|
238
|
+
const fitted = Math.min(metric.width, room - (getTableWidth(tight) - tableColumnWidth(metric)));
|
|
239
|
+
return fitted >= METRIC_FLOOR ? tight.map((column) => column === metric ? { ...column, width: fitted } : column) : null;
|
|
240
|
+
};
|
|
241
|
+
let shown = [...columns];
|
|
242
|
+
for (const drop of [null, ...(history ? ["change", "issued"] : ["issued"])]) {
|
|
243
|
+
if (drop) shown = shown.filter((column) => column.id !== drop);
|
|
244
|
+
const fitted = attempt(shown);
|
|
245
|
+
if (fitted) return fitted;
|
|
246
|
+
}
|
|
247
|
+
return narrow(columns);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Companies to point an empty pane at, one that has data and one for when the pane is for the first. */
|
|
251
|
+
const EXAMPLE_COMPANIES: Record<CompanyMode, readonly string[]> = { guidance: ["WMT", "DAL"], kpis: ["AAPL", "NVDA"] };
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* What an empty table says. A company with nothing on file for the function says so
|
|
255
|
+
* plainly and names one that has some; a selection or tab that narrows what the company
|
|
256
|
+
* has says what is missing from it. `cli` types `fn GUIDE WMT`, the app's command bar `GUIDE WMT`.
|
|
257
|
+
*/
|
|
258
|
+
export function companyEmptyState(mode: CompanyMode, symbol: string, view: { tab: string; hasRows: boolean }, surface: "cli" | "pane" = "pane"): { title: string; hint?: string } {
|
|
259
|
+
if (view.hasRows) return { title: view.tab === "history" ? "Actuals have not been matched to these guidance periods yet." : "No disclosed observations for this selection yet." };
|
|
260
|
+
const examples = EXAMPLE_COMPANIES[mode];
|
|
261
|
+
const example = examples.find((candidate) => candidate !== symbol.trim().toUpperCase()) ?? examples[0]!;
|
|
262
|
+
const command = `${mode === "kpis" ? "KPIS" : "GUIDE"} ${example}`;
|
|
263
|
+
return { title: `No ${mode === "kpis" ? "KPIs" : "guidance"} on file for ${symbol}.`, hint: `Try ${surface === "cli" ? `\`fn ${command}\`` : command}.` };
|
|
264
|
+
}
|
|
265
|
+
|
|
198
266
|
/** What a missing KPI means, as KPIS states it among its limitations and EQS beside a KPI column with gaps. */
|
|
199
267
|
export const KPI_COVERAGE_NOTE = "Coverage follows available company disclosures; missing data is not zero.";
|
|
200
268
|
|