@gloom-sh/terminal 0.19.1 → 0.19.3
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 +7 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +58 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +6 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +49 -6
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +191 -78
- 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/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +6 -4
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +19 -3
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +64 -42
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +5 -2
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/time-series/studies.ts +0 -1628
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getSharedRegistry } from "../../plugins/registry";
|
|
2
|
+
import { seriesExpression, seriesNamedBy } from "../../series/registry";
|
|
2
3
|
import { findCachedPortfolioAccount } from "../../plugins/builtin/portfolio-list/cached-account";
|
|
3
|
-
import { parsePublicTickerKey } from "../../utils/exchanges";
|
|
4
4
|
import { findNamedCollection } from "../../portfolio/collection-name";
|
|
5
5
|
import { readBareListing } from "../listing-arg";
|
|
6
6
|
import { apiClient } from "../../api-client";
|
|
7
|
+
import { resolveSavedInstrument } from "../../tickers/saved-instrument";
|
|
7
8
|
import type { MarketContext } from "../types";
|
|
8
9
|
import type {
|
|
9
10
|
HeadlessArgumentExpansion,
|
|
@@ -190,7 +191,7 @@ async function loadHeadlessPaneModel(
|
|
|
190
191
|
return result;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
type HeadlessMarketContext = Pick<MarketContext, "config" | "store" | "refresh" | "resolveBroker"> & Partial<Pick<MarketContext, "persistence" | "seriesCapabilities" | "findCatalogSeries">> & {
|
|
194
|
+
type HeadlessMarketContext = Pick<MarketContext, "config" | "store" | "refresh" | "resolveBroker"> & Partial<Pick<MarketContext, "persistence" | "seriesCapabilities" | "findCatalogSeries" | "findCatalogSeriesNamed">> & {
|
|
194
195
|
dataProvider: HeadlessPaneContext["marketData"];
|
|
195
196
|
};
|
|
196
197
|
|
|
@@ -210,6 +211,11 @@ export function createHeadlessPaneContext(
|
|
|
210
211
|
capabilities: getSharedRegistry() ?? context.seriesCapabilities,
|
|
211
212
|
catalogSeriesName: (expression) => (getSharedRegistry()?.series.findByExpression(expression)?.definition
|
|
212
213
|
?? context.findCatalogSeries?.(expression))?.name,
|
|
214
|
+
catalogSeriesNamed: (word) => {
|
|
215
|
+
const registered = getSharedRegistry()?.series?.list().map(({ definition }) => definition) ?? [];
|
|
216
|
+
const definition = seriesNamedBy(registered, word) ?? context.findCatalogSeriesNamed?.(word);
|
|
217
|
+
return definition ? { expression: seriesExpression(definition), name: definition.name } : undefined;
|
|
218
|
+
},
|
|
213
219
|
resolveBroker: context.resolveBroker,
|
|
214
220
|
async resolvePortfolio(id) {
|
|
215
221
|
const portfolio = findNamedCollection(context.config.portfolios, id);
|
|
@@ -222,25 +228,21 @@ export function createHeadlessPaneContext(
|
|
|
222
228
|
if (!watchlist) return null;
|
|
223
229
|
return (await context.store.loadAllTickers()).filter(row => row.metadata.watchlists.includes(watchlist.id));
|
|
224
230
|
},
|
|
225
|
-
|
|
226
|
-
const parsed = parsePublicTickerKey(key);
|
|
227
|
-
const ticker = await context.store.loadTicker(key)
|
|
228
|
-
?? (key !== parsed.symbol ? await context.store.loadTicker(parsed.symbol) : null);
|
|
229
|
-
return { symbol: parsed.symbol, exchange: parsed.exchange ?? ticker?.metadata.exchange };
|
|
230
|
-
},
|
|
231
|
+
resolveInstrument: (key) => resolveSavedInstrument(key, (saved) => context.store.loadTicker(saved)),
|
|
231
232
|
readDeviceState: <T,>(pluginId: string, key: string) =>
|
|
232
233
|
context.persistence?.pluginState.get<T>(pluginId, resumeStateKey(key))?.value ?? null,
|
|
233
234
|
async resolveListing(symbol, argumentWith) {
|
|
234
235
|
const command = `fn ${resolved.token}`;
|
|
235
236
|
const read = await readBareListing(symbol, context, {
|
|
236
237
|
command, noun: "a quote", retry: (key) => `gloom ${command} ${argumentWith(key)}`,
|
|
237
|
-
}, { refresh: !!context.refresh });
|
|
238
|
+
}, { refresh: !!context.refresh, successor: true });
|
|
238
239
|
if (!read) return null;
|
|
239
240
|
return {
|
|
240
241
|
symbol: read.symbol,
|
|
241
242
|
...read.exchange ? { exchange: read.exchange } : {},
|
|
242
243
|
...read.name ? { name: read.name } : {},
|
|
243
244
|
...read.coinNote ? { coinNote: read.coinNote } : {},
|
|
245
|
+
...read.successorNote ? { successorNote: read.successorNote } : {},
|
|
244
246
|
};
|
|
245
247
|
},
|
|
246
248
|
};
|
|
@@ -544,12 +546,17 @@ function renderRows(
|
|
|
544
546
|
stackPriority: column.stackPriority,
|
|
545
547
|
stackRows: column.stackRows,
|
|
546
548
|
minTableWidth: column.minTableWidth,
|
|
549
|
+
optional: column.optional,
|
|
550
|
+
dropPriority: column.dropPriority,
|
|
547
551
|
})),
|
|
548
552
|
rows.map((row) => columns.map((column) => cell(row, column))),
|
|
549
553
|
{
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
554
|
+
// A value the text leaves out (a summary that only repeats the headline) takes no line.
|
|
555
|
+
below: under.length === 0 ? undefined : rows.map((row) => under.flatMap((column) => {
|
|
556
|
+
const text = row[column.key] == null || row[column.key] === "" ? "" : cell(row, column);
|
|
557
|
+
return text === "" ? [] : text.split("\n");
|
|
558
|
+
})),
|
|
559
|
+
...(under.length > 0 && under.every((column) => column.underRow === "line") ? { cutBelow: true } : {}),
|
|
553
560
|
},
|
|
554
561
|
);
|
|
555
562
|
}
|
|
@@ -571,7 +578,7 @@ function renderBundle(
|
|
|
571
578
|
return result.sections.filter((section) => !isLockedSection(section)).flatMap((section, index) => [
|
|
572
579
|
...(index > 0 ? [""] : []),
|
|
573
580
|
// A section with no title is its rows, or its empty message, alone.
|
|
574
|
-
...(section.title ? [renderSection(section.title)] : []),
|
|
581
|
+
...(section.title ? [renderSection(section.textTitle ?? section.title)] : []),
|
|
575
582
|
"rows" in section && section.rows
|
|
576
583
|
? renderRows(section.textRows ?? section.rows, section.textColumns ?? section.columns, definition.columns, section.empty)
|
|
577
584
|
: renderEntries(section.textEntries ?? section.entries),
|
|
@@ -898,7 +905,8 @@ export function renderHeadlessPaneText(
|
|
|
898
905
|
): string {
|
|
899
906
|
// A long title wraps to a narrow --width like the text under a table; each line stays bold.
|
|
900
907
|
const lines = [...wrapToTerminal(reportTitle(definition, args, result, fallbackTitle)).split("\n").map(cliStyles.bold), ""];
|
|
901
|
-
|
|
908
|
+
// A notice wraps to a narrow --width, as the notes and errors under the tables do.
|
|
909
|
+
const notices = textNotices(result).map((notice) => wrapToTerminal(notice));
|
|
902
910
|
if (notices.length) lines.push(...notices, "");
|
|
903
911
|
const gate = reportGate(definition, result);
|
|
904
912
|
const reason = emptyReportReason(definition, result, args, token, gate);
|
|
@@ -46,6 +46,7 @@ import { checkOutputPath, resolveOutputPath, writeOutputFile } from "../output-f
|
|
|
46
46
|
import type { CatalogCommandHit } from "../unknown-command";
|
|
47
47
|
import { CLI_EXIT_CODES } from "../exit-codes";
|
|
48
48
|
import { shotOptionRefusal } from "./shot-options";
|
|
49
|
+
import { listingChangeLine } from "../../tickers/listing-fate";
|
|
49
50
|
import {
|
|
50
51
|
GLOSSARY,
|
|
51
52
|
glossaryRecord,
|
|
@@ -312,7 +313,7 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
|
312
313
|
let quoted: ReturnType<typeof loadListingQuoteAnswer> | null = null;
|
|
313
314
|
const quoteAnswer = listing ? () => quoted ??= loadListingQuoteAnswer(context.dataProvider, listing) : null;
|
|
314
315
|
const fate = listing && quoteAnswer && isTickerLinkPeer(resolved.instance, registry.panes)
|
|
315
|
-
? listingFateLoader(listing, context, quoteAnswer().then((answer) => answer.quote))
|
|
316
|
+
? listingFateLoader(listing, context, quoteAnswer().then((answer) => answer.quote), { successor: true })
|
|
316
317
|
: null;
|
|
317
318
|
const moved = { command: "fn", retry: (key: string) => `gloom fn ${resolved.token} ${key}` };
|
|
318
319
|
// A report that fails or comes back empty for an exchange the symbol is not listed on says so.
|
|
@@ -356,7 +357,10 @@ export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
|
356
357
|
const data = glossary
|
|
357
358
|
? { ...report.data, glossary: glossary.map(({ term, short, definition }) => ({ term, ...(short ? { short } : {}), definition })) }
|
|
358
359
|
: report.data;
|
|
359
|
-
|
|
360
|
+
// What became of the company that had the symbol (GOGL), as `quote` says it, leads.
|
|
361
|
+
const successor = fate ? listingChangeLine(await fate()) : null;
|
|
362
|
+
// A note the listing and the report both make (a coin's code charted as a fund) is said once.
|
|
363
|
+
const warnings = [...new Set([...successor ? [successor] : [], ...parsed.listingNotes ?? [], ...await choice ?? [], ...report.data.warnings ?? []])];
|
|
360
364
|
// A report with no data is printed as it is, and fails: exit 1, and `ok: false` in JSON with the reason.
|
|
361
365
|
const failure = reportFailure(report.data, ["fn", resolved.token, parsed.arg.trim()].filter(Boolean).join(" "), {
|
|
362
366
|
failsWithoutData: resolved.headless?.failsWithoutData, tables: report.tables });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HeadlessPaneDefinition, PaneTemplateCreateOptions, PaneTemplateDef } from "../../types/plugin";
|
|
1
|
+
import type { HeadlessPaneDefinition, PaneDef, PaneTemplateCreateOptions, PaneTemplateDef } from "../../types/plugin";
|
|
2
2
|
import { parseTickerListInput } from "../../tickers/list";
|
|
3
3
|
import { normalizeTickerInput } from "../../tickers/search";
|
|
4
4
|
import { resolveFinancialPeriodOption } from "../../plugins/builtin/ticker-detail/financials/model";
|
|
@@ -85,12 +85,16 @@ export function cleanTickerInput(value: string): string {
|
|
|
85
85
|
/**
|
|
86
86
|
* Whether a function's argument names the ticker its pane shows. Its report
|
|
87
87
|
* says first: AUCT's `10Y` and IPO's `pending` are free text its own resolver
|
|
88
|
-
* reads, never a symbol to look up. Without a report,
|
|
88
|
+
* reads, never a symbol to look up. Without a report, a rendered pane whose
|
|
89
|
+
* argument is an id it opens (the story pane's story id) says so, then the
|
|
90
|
+
* shortcut.
|
|
89
91
|
*/
|
|
90
92
|
export function argumentNamesTicker(
|
|
91
93
|
template: Pick<PaneTemplateDef, "shortcut"> | undefined,
|
|
92
94
|
headless: Pick<HeadlessPaneDefinition, "argument"> | undefined,
|
|
95
|
+
pane?: Pick<PaneDef, "reportArgument">,
|
|
93
96
|
): boolean {
|
|
97
|
+
if (!headless && pane?.reportArgument?.identifier) return false;
|
|
94
98
|
const kind = headless?.argument.kind;
|
|
95
99
|
if (kind === "free-text" || kind === "none") return false;
|
|
96
100
|
return kind === "tickers" || kind === "symbol-list" || template?.shortcut?.argKind !== "text";
|
|
@@ -11,7 +11,7 @@ import { applyChartComposerCapabilityOptions } from "../../plugins/builtin/chart
|
|
|
11
11
|
import { parseChartSpec } from "../../plugins/builtin/chart-composer/chart-spec";
|
|
12
12
|
import { normalizeTickerInput } from "../../tickers/search";
|
|
13
13
|
import { slugifyName } from "../../utils/slugify";
|
|
14
|
-
import { ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
|
|
14
|
+
import { listCoinNotes, ListingArgError, listingInstrument, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
|
|
15
15
|
import { uncoveredInstrumentKind, uncoveredInstrumentMessage } from "../../tickers/instrument-coverage";
|
|
16
16
|
import { uncoveredInstrumentCommand } from "../uncovered-instrument";
|
|
17
17
|
import { listingAbroadVenue } from "../../utils/sec";
|
|
@@ -83,7 +83,7 @@ async function buildPaneInstance(
|
|
|
83
83
|
// Free text is a phrase, not a symbol. Treating it as a ticker makes the
|
|
84
84
|
// pane look for a provider for values such as "HIGH GROWTH SOFTWARE".
|
|
85
85
|
const headlessArgKind = resolved.headless?.argument.kind;
|
|
86
|
-
const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless)
|
|
86
|
+
const fallbackSymbol = !argumentNamesTicker(resolved.template, resolved.headless, resolved.pane)
|
|
87
87
|
? null
|
|
88
88
|
: headlessArgKind === "tickers" || headlessArgKind === "symbol-list"
|
|
89
89
|
? normalizeTickerInput(null, cleanTickerInput(target.split(/[,\s]+/)[0] ?? ""))
|
|
@@ -254,8 +254,9 @@ export async function applyListingArgument(
|
|
|
254
254
|
}
|
|
255
255
|
notes.push(...answers.map((answer) => `${answer.message} ${answer.details.replaceAll("\n", " ")}`));
|
|
256
256
|
}
|
|
257
|
-
// One listing's notes come with its choice note; a list's are said here
|
|
258
|
-
|
|
257
|
+
// One listing's notes come with its choice note; a list's are said here, with a coin's code read as a listing
|
|
258
|
+
// (CORR RIOT,BTC: BTC is the Grayscale fund) as `quote BTC` says it.
|
|
259
|
+
if (listings.length > 1) notes.unshift(...listings.flatMap(listingReadNotes), ...await listCoinNotes(listings, context));
|
|
259
260
|
return {
|
|
260
261
|
...args,
|
|
261
262
|
arg: listings.map((listing) => listing.key).join(", "),
|
|
@@ -349,8 +350,8 @@ export async function resolvePaneFunction(
|
|
|
349
350
|
if (!pane) {
|
|
350
351
|
throw new Error(`Template "${template?.id}" points at missing pane "${template?.paneId}".`);
|
|
351
352
|
}
|
|
352
|
-
const createOptions = buildCreateOptions(template, args.arg);
|
|
353
353
|
const headless = getHeadlessPaneDefinition(template, pane);
|
|
354
|
+
const createOptions = withTickerList(template, buildCreateOptions(template, args.arg), headless, args.arg, paneFunctionCode(template, pane));
|
|
354
355
|
const capability = getPaneFunctionCapability(template, pane);
|
|
355
356
|
let tableSection: string | true | undefined;
|
|
356
357
|
if (resolutionSettings.tableSection) {
|
|
@@ -400,6 +401,27 @@ export async function resolvePaneFunction(
|
|
|
400
401
|
return resolved;
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
/**
|
|
405
|
+
* A one-ticker template whose pane also opens on a list (`opensTickerList`:
|
|
406
|
+
* DVD's ranking): the list is passed as `symbols`, so `shot` draws the same
|
|
407
|
+
* tickers `fn` reads, in one pane, and fetches each; past the most the
|
|
408
|
+
* function's report takes, it is refused as `fn` refuses it. One ticker, and
|
|
409
|
+
* any other template, stays as it was.
|
|
410
|
+
*/
|
|
411
|
+
export function withTickerList(
|
|
412
|
+
template: PaneTemplateDef | undefined,
|
|
413
|
+
createOptions: PaneTemplateCreateOptions | undefined,
|
|
414
|
+
headless: HeadlessPaneDefinition | undefined,
|
|
415
|
+
arg: string,
|
|
416
|
+
token = template?.shortcut?.prefix ?? "",
|
|
417
|
+
): PaneTemplateCreateOptions | undefined {
|
|
418
|
+
if (!template?.opensTickerList || !createOptions || createOptions.symbols?.length) return createOptions;
|
|
419
|
+
const symbols = [...new Set(arg.split(/[,\s]+/).map((word) => cleanTickerInput(word)).filter(Boolean))];
|
|
420
|
+
if (symbols.length < 2) return createOptions;
|
|
421
|
+
if (headless) validateSymbolCount(headless, token, symbols, "shot");
|
|
422
|
+
return { ...createOptions, symbol: symbols[0]!, arg: symbols.join(","), symbols };
|
|
423
|
+
}
|
|
424
|
+
|
|
403
425
|
function validateTickerCardinality(
|
|
404
426
|
capability: PaneFunctionCapability,
|
|
405
427
|
createOptions: PaneTemplateCreateOptions | undefined,
|
|
@@ -208,6 +208,39 @@ export function filledKeyValueCount(rows: DesktopPaneShotRenderResult["visibleKe
|
|
|
208
208
|
}).length;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Whether a capture shows nothing that was loaded: a wall that needs an
|
|
213
|
+
* account, or an empty, loading or failed state, with no rows, labeled values
|
|
214
|
+
* or plotted chart beside it. Its footer carries no status line then, as it
|
|
215
|
+
* has no data to date: the quotes the capture loaded for its symbol are not on
|
|
216
|
+
* screen. A pane that only locks or fails part of what it shows keeps it.
|
|
217
|
+
*/
|
|
218
|
+
export function shotLoadedNothing(
|
|
219
|
+
render: Pick<DesktopPaneShotRenderResult, "accessGate" | "emptyStateDetected" | "loadingStateDetected" | "errorStateDetected" | "rows" | "visibleKeyValues" | "semanticUi">,
|
|
220
|
+
): boolean {
|
|
221
|
+
const stated = !!render.accessGate || render.emptyStateDetected || render.loadingStateDetected || render.errorStateDetected;
|
|
222
|
+
return stated
|
|
223
|
+
&& render.rows.length === 0
|
|
224
|
+
&& filledKeyValueCount(render.visibleKeyValues) === 0
|
|
225
|
+
&& !render.semanticUi.some(plotsData);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A pane's chart evidence node that says it plotted something: a count of
|
|
230
|
+
* plotted values, or series with points. A node that is neither (a shape this
|
|
231
|
+
* does not know) counts as plotted, so its pane keeps its line.
|
|
232
|
+
*/
|
|
233
|
+
function plotsData(node: RemoteUiNodeSnapshot): boolean {
|
|
234
|
+
if (node.role !== "chart-data") return false;
|
|
235
|
+
const metadata = node.metadata;
|
|
236
|
+
if (!isRecord(metadata)) return true;
|
|
237
|
+
const counts = [metadata.plottedValueCount, metadata.projectedPointCount].filter((count): count is number => typeof count === "number");
|
|
238
|
+
if (counts.length > 0) return counts.some((count) => count > 0);
|
|
239
|
+
const lists = [metadata.series, metadata.baseSeries, metadata.resolvedSeries].filter(Array.isArray);
|
|
240
|
+
if (lists.length === 0) return true;
|
|
241
|
+
return lists.some((list) => list.some((series) => isRecord(series) && typeof series.pointCount === "number" && series.pointCount > 0));
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/**
|
|
212
245
|
* A 3D surface that is not the real drawing: the frame has no mesh, or the 2D
|
|
213
246
|
* grid stands in for it. Both are fine for a person using the pane and wrong
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { lookup } from "dns/promises";
|
|
2
|
+
import { isIP } from "net";
|
|
1
3
|
import { fetchTape } from "../../plugins/builtin/time-sales/client";
|
|
2
4
|
import {
|
|
5
|
+
encodeResponseBody,
|
|
3
6
|
readRequestInit,
|
|
4
|
-
|
|
7
|
+
toResponseHead,
|
|
5
8
|
type HttpProxyRequestEnvelope,
|
|
6
9
|
type HttpProxyResponseEnvelope,
|
|
7
10
|
} from "../../utils/http-proxy-response";
|
|
11
|
+
import { fetchCheckingRedirects, readBytesWithin } from "../../utils/http-proxy-upstream";
|
|
12
|
+
import { isPublicAddress } from "../../utils/public-address";
|
|
8
13
|
import type { DesktopPaneShotApiProxy, DesktopPaneShotBridge } from "../desktop-pane-shot";
|
|
9
14
|
import type { MarketContext } from "../types";
|
|
10
15
|
import { resolvePersistedCloudSessionToken } from "./cloud-session";
|
|
11
16
|
import { getCloudApiBaseUrl } from "../../api-client/request";
|
|
17
|
+
import { fetchShotHttpAddress } from "./screenshot-http";
|
|
12
18
|
|
|
13
19
|
const CREDENTIAL_FIELD_NAMES = new Set([
|
|
14
20
|
"accesstoken",
|
|
@@ -98,10 +104,27 @@ const SHOT_BRIDGE_MARKET_OPERATIONS = new Set([
|
|
|
98
104
|
]);
|
|
99
105
|
|
|
100
106
|
const SHOT_BRIDGE_HTTP_TIMEOUT_MS = 20_000;
|
|
107
|
+
/** Feeds and API answers are far smaller; the answer goes to the page whole, as JSON. */
|
|
108
|
+
const SHOT_BRIDGE_HTTP_MAX_BYTES = 25 * 1024 * 1024;
|
|
109
|
+
/** What panes send through `httpFetch` while they draw: reads, and the POST a few APIs take a query as. */
|
|
110
|
+
const SHOT_BRIDGE_HTTP_METHODS = new Set(["GET", "HEAD", "POST"]);
|
|
111
|
+
|
|
112
|
+
/** The addresses a host name resolves to. */
|
|
113
|
+
type ResolveHost = (hostname: string) => Promise<string[]>;
|
|
114
|
+
|
|
115
|
+
const resolveHost: ResolveHost = async (hostname) => (
|
|
116
|
+
await lookup(hostname, { all: true, verbatim: true })
|
|
117
|
+
).map((entry) => entry.address);
|
|
118
|
+
|
|
119
|
+
interface ShotHttpOptions {
|
|
120
|
+
resolveHost?: ResolveHost;
|
|
121
|
+
fetch?: typeof fetch;
|
|
122
|
+
}
|
|
101
123
|
|
|
102
124
|
export function createDesktopShotBridge(
|
|
103
125
|
context: Pick<MarketContext, "dataProvider">,
|
|
104
126
|
tapeBootstrap: typeof fetchTape = fetchTape,
|
|
127
|
+
httpOptions: ShotHttpOptions = {},
|
|
105
128
|
): DesktopPaneShotBridge {
|
|
106
129
|
return {
|
|
107
130
|
async marketData(operation, args) {
|
|
@@ -123,30 +146,88 @@ export function createDesktopShotBridge(
|
|
|
123
146
|
args,
|
|
124
147
|
);
|
|
125
148
|
},
|
|
126
|
-
httpFetch: (request) => runDesktopShotHttpFetch(request),
|
|
149
|
+
httpFetch: (request) => runDesktopShotHttpFetch(request, httpOptions),
|
|
127
150
|
};
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Whether the page may reach `raw`: http or https on a host whose every
|
|
155
|
+
* address is on the public internet. Panes read public APIs and news feeds;
|
|
156
|
+
* nothing they draw lives on this machine or its network, and a URL that
|
|
157
|
+
* names one (or a host that resolves to one) is refused.
|
|
158
|
+
*/
|
|
159
|
+
async function checkShotHttpTarget(
|
|
160
|
+
raw: string,
|
|
161
|
+
resolve: ResolveHost,
|
|
162
|
+
): Promise<{ url: URL; address: string } | { error: string; status: number }> {
|
|
163
|
+
let url: URL;
|
|
164
|
+
try {
|
|
165
|
+
url = new URL(raw);
|
|
166
|
+
} catch {
|
|
167
|
+
// Intentional ignore: a URL that does not parse is answered as an invalid target, never fetched.
|
|
168
|
+
return { error: "The target URL is not valid.", status: 400 };
|
|
169
|
+
}
|
|
170
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
171
|
+
return { error: `Screenshot HTTP bridge refuses ${url.protocol} requests.`, status: 400 };
|
|
172
|
+
}
|
|
173
|
+
if (url.username || url.password) return { error: "Credentials in the URL are not allowed.", status: 400 };
|
|
174
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
175
|
+
const refused = { error: `Screenshot HTTP bridge refuses ${url.hostname}: it is not on the public internet.`, status: 403 };
|
|
176
|
+
if (host === "localhost" || host.endsWith(".localhost")) return refused;
|
|
177
|
+
let addresses: string[];
|
|
178
|
+
try {
|
|
179
|
+
addresses = isIP(host) ? [host] : await resolve(host);
|
|
180
|
+
} catch {
|
|
181
|
+
return { error: `Could not resolve ${url.hostname}.`, status: 502 };
|
|
182
|
+
}
|
|
183
|
+
if (addresses.length === 0) return { error: `Could not resolve ${url.hostname}.`, status: 502 };
|
|
184
|
+
if (!addresses.every(isPublicAddress)) return refused;
|
|
185
|
+
const address = addresses.find((candidate) => isIP(candidate) === 4) ?? addresses[0]!;
|
|
186
|
+
return { url, address };
|
|
187
|
+
}
|
|
188
|
+
|
|
130
189
|
/**
|
|
131
190
|
* Panes such as DVD fetch a third-party API directly. A browser cannot: the
|
|
132
191
|
* request is cross-origin and dies in CORS, so the pane reported that the
|
|
133
192
|
* ticker pays no dividend. The desktop renderer already proxies these through
|
|
134
|
-
* its native half; the screenshot page gets the same treatment
|
|
193
|
+
* its native half; the screenshot page gets the same treatment, limited to
|
|
194
|
+
* public hosts, each redirect checked like the first URL.
|
|
135
195
|
*/
|
|
136
196
|
async function runDesktopShotHttpFetch(
|
|
137
197
|
request: HttpProxyRequestEnvelope,
|
|
198
|
+
options: ShotHttpOptions,
|
|
138
199
|
): Promise<HttpProxyResponseEnvelope> {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
const resolve = options.resolveHost ?? resolveHost;
|
|
201
|
+
const addresses = new Map<string, string>();
|
|
202
|
+
const checkTarget = async (raw: string) => {
|
|
203
|
+
const checked = await checkShotHttpTarget(raw, resolve);
|
|
204
|
+
if ("url" in checked) addresses.set(checked.url.href, checked.address);
|
|
205
|
+
return checked;
|
|
206
|
+
};
|
|
207
|
+
const target = await checkTarget(typeof request?.url === "string" ? request.url : "");
|
|
208
|
+
if ("error" in target) throw new Error(target.error);
|
|
209
|
+
const init = readRequestInit(request.init);
|
|
210
|
+
if (!SHOT_BRIDGE_HTTP_METHODS.has(init.method)) {
|
|
211
|
+
throw new Error(`Screenshot HTTP bridge refuses ${init.method} requests.`);
|
|
142
212
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
const result = await fetchCheckingRedirects(target.url, init, {
|
|
214
|
+
fetch: options.fetch ?? (async (input, init) => {
|
|
215
|
+
const url = new URL(String(input));
|
|
216
|
+
const address = addresses.get(url.href);
|
|
217
|
+
if (!address) throw new Error("The screenshot target has not been checked.");
|
|
218
|
+
return fetchShotHttpAddress(url, init ?? {}, address);
|
|
219
|
+
}) as typeof fetch,
|
|
220
|
+
check: checkTarget,
|
|
149
221
|
signal: AbortSignal.timeout(SHOT_BRIDGE_HTTP_TIMEOUT_MS),
|
|
150
222
|
});
|
|
151
|
-
|
|
223
|
+
if ("error" in result) throw new Error(result.error);
|
|
224
|
+
const { response } = result;
|
|
225
|
+
const tooLarge = `The response from ${target.url.hostname} is larger than ${SHOT_BRIDGE_HTTP_MAX_BYTES / (1024 * 1024)} MB.`;
|
|
226
|
+
if (init.method !== "HEAD" && Number(response.headers.get("content-length") ?? 0) > SHOT_BRIDGE_HTTP_MAX_BYTES) {
|
|
227
|
+
await response.body?.cancel().catch(() => { /* Intentional ignore: an answer over the cap is dropped unread. */ });
|
|
228
|
+
throw new Error(tooLarge);
|
|
229
|
+
}
|
|
230
|
+
const body = await readBytesWithin(response.body, SHOT_BRIDGE_HTTP_MAX_BYTES);
|
|
231
|
+
if (body === null) throw new Error(tooLarge);
|
|
232
|
+
return { ...toResponseHead(response), ...encodeResponseBody(body) };
|
|
152
233
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { checkServerIdentity } from "node:tls";
|
|
6
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A single HTTP hop to an address already checked by the screenshot bridge.
|
|
10
|
+
* The socket uses that address directly, while Host and TLS keep the URL's
|
|
11
|
+
* name. A fresh connection keeps another request's DNS result out of the hop.
|
|
12
|
+
*/
|
|
13
|
+
export function fetchShotHttpAddress(url: URL, init: RequestInit, address: string): Promise<Response> {
|
|
14
|
+
if (!isIP(address)) return Promise.reject(new Error("The screenshot address must be an IP address."));
|
|
15
|
+
const headers = new Headers(init.headers);
|
|
16
|
+
headers.set("host", url.host);
|
|
17
|
+
headers.set("accept-encoding", "gzip, deflate, br");
|
|
18
|
+
if (!headers.has("accept")) headers.set("accept", "*/*");
|
|
19
|
+
if (!headers.has("user-agent")) headers.set("user-agent", `Bun/${Bun.version}`);
|
|
20
|
+
if (typeof init.body === "string" && !headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
|
|
21
|
+
const requestHeaders: Record<string, string> = {};
|
|
22
|
+
headers.forEach((value, name) => { requestHeaders[name] = value; });
|
|
23
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
26
|
+
hostname: address,
|
|
27
|
+
method: init.method,
|
|
28
|
+
headers: requestHeaders,
|
|
29
|
+
agent: false,
|
|
30
|
+
signal: init.signal ?? undefined,
|
|
31
|
+
...(url.protocol === "https:" ? {
|
|
32
|
+
servername: isIP(hostname) ? "" : hostname,
|
|
33
|
+
checkServerIdentity: (_name: string, certificate: Parameters<typeof checkServerIdentity>[1]) => checkServerIdentity(hostname, certificate),
|
|
34
|
+
} : {}),
|
|
35
|
+
}, (incoming) => {
|
|
36
|
+
try {
|
|
37
|
+
const responseHeaders = new Headers();
|
|
38
|
+
for (let index = 0; index < incoming.rawHeaders.length; index += 2) {
|
|
39
|
+
responseHeaders.append(incoming.rawHeaders[index]!, incoming.rawHeaders[index + 1]!);
|
|
40
|
+
}
|
|
41
|
+
const status = incoming.statusCode ?? 502;
|
|
42
|
+
if (init.method === "HEAD" || status === 204 || status === 205 || status === 304) {
|
|
43
|
+
incoming.resume();
|
|
44
|
+
resolve(new Response(null, { status, statusText: incoming.statusMessage, headers: responseHeaders }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const encoding = responseHeaders.get("content-encoding")?.toLowerCase();
|
|
48
|
+
const decoder = encoding === "gzip" ? createGunzip()
|
|
49
|
+
: encoding === "deflate" ? createInflate()
|
|
50
|
+
: encoding === "br" ? createBrotliDecompress() : null;
|
|
51
|
+
if (decoder) {
|
|
52
|
+
responseHeaders.delete("content-encoding");
|
|
53
|
+
responseHeaders.delete("content-length");
|
|
54
|
+
incoming.on("error", (error) => decoder.destroy(error));
|
|
55
|
+
decoder.once("close", () => incoming.destroy());
|
|
56
|
+
}
|
|
57
|
+
const stream = decoder ? incoming.pipe(decoder) : incoming;
|
|
58
|
+
resolve(new Response(Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>, {
|
|
59
|
+
status, statusText: incoming.statusMessage, headers: responseHeaders,
|
|
60
|
+
}));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
incoming.destroy();
|
|
63
|
+
reject(error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
request.once("error", reject);
|
|
67
|
+
if (init.body !== undefined && init.body !== null) request.write(init.body);
|
|
68
|
+
request.end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
2
|
import { apiClient } from "../../api-client";
|
|
3
3
|
import { loadResolvedHeadlessPaneModel, reportCoverage, reportGaps, type LoadedHeadlessPaneModel } from "./headless";
|
|
4
|
-
import {
|
|
5
|
-
import { mkdir } from "fs/promises";
|
|
4
|
+
import { createOutputFolder } from "../output-file";
|
|
6
5
|
import type { Quote } from "../../types/financials";
|
|
7
6
|
import { isRecord } from "../../utils/guards";
|
|
8
7
|
import {
|
|
@@ -28,7 +27,18 @@ import { quotesFreshness } from "../freshness";
|
|
|
28
27
|
import { paneFitsRows, shotMaxHeightCells, buildDesktopShotPayload } from "./screenshot-payload";
|
|
29
28
|
import { resolveDesktopShotApiProxy, createDesktopShotBridge } from "./screenshot-bridge";
|
|
30
29
|
import type { PaneScreenshotResult } from "./screenshot-types";
|
|
31
|
-
import { assessPaneScreenshot } from "./screenshot-assessment";
|
|
30
|
+
import { assessPaneScreenshot, shotLoadedNothing } from "./screenshot-assessment";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether a capture's footer keeps a row for the dated status line. A pane of
|
|
34
|
+
* the user's own state (notes, connections, documentation) declares `local`:
|
|
35
|
+
* it has no data to date, and "not a live feed" would be said of its own
|
|
36
|
+
* state. A capture that loaded nothing gives the row back later
|
|
37
|
+
* (`shotLoadedNothing`).
|
|
38
|
+
*/
|
|
39
|
+
export function shotHasStatusLine(resolved: Pick<ResolvedPaneFunction, "pane" | "template">): boolean {
|
|
40
|
+
return !declaredReportFreshness(resolved).local;
|
|
41
|
+
}
|
|
32
42
|
|
|
33
43
|
/** The bridge, keeping every quote it serves the page: what a pane that loads its own quotes (WEI) shows. */
|
|
34
44
|
function recordBridgedQuotes(bridge: DesktopPaneShotBridge): { bridge: DesktopPaneShotBridge; quotes: Quote[] } {
|
|
@@ -241,7 +251,7 @@ export async function renderDesktopShot({
|
|
|
241
251
|
*/
|
|
242
252
|
onStalledReport?: () => void;
|
|
243
253
|
}): Promise<PaneScreenshotResult> {
|
|
244
|
-
if (captureImage) await
|
|
254
|
+
if (captureImage) await createOutputFolder(outputPath);
|
|
245
255
|
const apiProxy = resolveDesktopShotApiProxy(context);
|
|
246
256
|
const previousSessionToken = apiClient.getSessionToken();
|
|
247
257
|
let payload: DesktopPaneShotPayload;
|
|
@@ -277,7 +287,7 @@ export async function renderDesktopShot({
|
|
|
277
287
|
watermark ?? null,
|
|
278
288
|
loadModel,
|
|
279
289
|
);
|
|
280
|
-
if (statusLine && captureImage) payload.statusLine = true;
|
|
290
|
+
if (statusLine && captureImage && shotHasStatusLine(resolved)) payload.statusLine = true;
|
|
281
291
|
const fitRoles = [
|
|
282
292
|
...(fitRows && paneFitsRows(resolved) ? ["data-table-body-scroll"] : []),
|
|
283
293
|
...(fitReader && resolved.pane.shotFitsReader ? ["detail-scroll-body"] : []),
|
|
@@ -296,6 +306,10 @@ export async function renderDesktopShot({
|
|
|
296
306
|
captureImage,
|
|
297
307
|
bridge: recorded.bridge,
|
|
298
308
|
statusLine: async (rendered) => {
|
|
309
|
+
if (shotLoadedNothing(rendered)) {
|
|
310
|
+
abandonReports();
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
299
313
|
const tab = report && shownReportTab(resolved, rendered.semanticUi);
|
|
300
314
|
const shown = tab ? reportShotFreshness(resolved, () => loadReport({ ...resolved, options: { ...resolved.options, tab } })) : report;
|
|
301
315
|
const line = shotFreshness(resolved, shotPayload, rendered, recorded.quotes, shown && await settledWithin(shown, SHOT_REPORT_WAIT_MS));
|
package/src/cli/quote-answer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { QuoteBatchResult } from "../types/data-provider";
|
|
|
2
2
|
import type { Quote } from "../types/financials";
|
|
3
3
|
import { bareListingNote } from "../tickers/listing-fate";
|
|
4
4
|
import { notATickerSymbol } from "../sources/provider-errors";
|
|
5
|
+
import { listingSuffixCandidate } from "../sources/listing-symbols";
|
|
5
6
|
import { looksLikeCurrencyPair } from "../utils/currency-pair";
|
|
6
7
|
import { canonicalExchange } from "../utils/exchanges";
|
|
7
8
|
import type { CliCommandDef } from "../types/plugin";
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
notTradedMessage,
|
|
22
23
|
oneWordCompanyName,
|
|
23
24
|
quoteBareListing,
|
|
25
|
+
serviceNamedCompany,
|
|
26
|
+
type ListingArg,
|
|
24
27
|
requireCliListing,
|
|
25
28
|
resolveCliListing,
|
|
26
29
|
tradingBoardNote,
|
|
@@ -64,7 +67,9 @@ export interface QuoteAnswer {
|
|
|
64
67
|
/**
|
|
65
68
|
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
66
69
|
* is not listed on says so. A bare symbol the data service read on its home
|
|
67
|
-
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes
|
|
70
|
+
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes that
|
|
71
|
+
* the service marks as another listing's company name is that company
|
|
72
|
+
* (`serviceNamedCompany`: "siemens" is SIE:XETRA); else it is
|
|
68
73
|
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
69
74
|
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
70
75
|
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
@@ -98,6 +103,13 @@ export async function answerQuote(
|
|
|
98
103
|
const notTraded = await notTradedMessage(listing, market);
|
|
99
104
|
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
100
105
|
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
106
|
+
// HCI.L is HCI on LSE written with a vendor's suffix: one that does not trade there says so as HCI:LSE does.
|
|
107
|
+
const suffixed = await notTradedOnSuffixVenue(listing, market);
|
|
108
|
+
if (suffixed) return { result, listing, failure: { message: suffixed, oneLine: suffixed }, notes: [] };
|
|
109
|
+
// A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
|
|
110
|
+
const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
|
|
111
|
+
const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
|
|
112
|
+
if (meant) return meant;
|
|
101
113
|
const other = await quoteBareListing(listing, market, request, { refresh, datesChangeBasis });
|
|
102
114
|
if (other?.kind === "quoted") {
|
|
103
115
|
return {
|
|
@@ -129,6 +141,22 @@ export async function answerQuote(
|
|
|
129
141
|
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
/**
|
|
145
|
+
* "HCI does not trade on LSE. HCI trades on: NYSE, IEX, JSE." for a symbol
|
|
146
|
+
* written with a venue's suffix (HCI.L) that no source quotes, when its base
|
|
147
|
+
* symbol trades elsewhere and not on the venue the suffix names: the answer
|
|
148
|
+
* `HCI:LSE` gets. Only for London's `.L` and suffixes of two letters or more
|
|
149
|
+
* (`.IS`, `.JO`): another one-letter suffix is as likely a US share class
|
|
150
|
+
* (MKC.V, BRK.B) and keeps its own answer. Null otherwise.
|
|
151
|
+
*/
|
|
152
|
+
async function notTradedOnSuffixVenue(listing: CliListing, market: MarketContext): Promise<string | null> {
|
|
153
|
+
if (listing.exchange) return null;
|
|
154
|
+
const suffix = listing.symbol.slice(listing.symbol.lastIndexOf(".") + 1);
|
|
155
|
+
if (!listing.symbol.includes(".") || (suffix.length < 2 && suffix.toUpperCase() !== "L")) return null;
|
|
156
|
+
const venue = listingSuffixCandidate(listing.symbol);
|
|
157
|
+
return venue ? notTradedMessage(venue, market) : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
132
160
|
/**
|
|
133
161
|
* A company's name typed as one word (`Apple`) that the source says is not a
|
|
134
162
|
* ticker, answered as the listing search names for it with the line saying so
|
|
@@ -155,17 +183,32 @@ async function answerCompanyName(
|
|
|
155
183
|
const named = await oneWordCompanyName(listing.typed, market);
|
|
156
184
|
// A real ticker a source failed to quote is not a company's name, even when the company carries the word.
|
|
157
185
|
if (!named || (unsettled && named.symbol.toUpperCase() === listing.symbol.toUpperCase())) return null;
|
|
158
|
-
|
|
186
|
+
const answer = await answerNamedListing(named, market, request, refresh, datesChangeBasis);
|
|
187
|
+
if (answer) return answer;
|
|
188
|
+
if (!unsettled) return null;
|
|
189
|
+
const sentence = await notATickerWords(listing.typed, market);
|
|
190
|
+
return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The row of the listing a company's name was read as (`named`), saying so;
|
|
195
|
+
* null when the app cannot read its venue or it has no quote.
|
|
196
|
+
*/
|
|
197
|
+
async function answerNamedListing(
|
|
198
|
+
named: ListingArg,
|
|
199
|
+
market: MarketContext,
|
|
200
|
+
request: ListingDataRequest,
|
|
201
|
+
refresh: boolean,
|
|
202
|
+
datesChangeBasis: boolean,
|
|
203
|
+
): Promise<QuoteAnswer | null> {
|
|
204
|
+
// A listing on a venue the app cannot read is no answer: the word keeps its own.
|
|
159
205
|
const resolved = await resolveCliListing(named.key, null, market).catch((error: unknown) => {
|
|
160
206
|
if (error instanceof ListingArgError) return null;
|
|
161
207
|
throw error;
|
|
162
208
|
});
|
|
163
209
|
const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
|
|
164
210
|
const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
|
|
165
|
-
|
|
166
|
-
if (!unsettled) return null;
|
|
167
|
-
const sentence = await notATickerWords(listing.typed, market);
|
|
168
|
-
return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
|
|
211
|
+
return found && quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
|
|
169
212
|
}
|
|
170
213
|
|
|
171
214
|
/** A listing a command found a quote for, with what `quote` says beside it. */
|