@gloom-sh/terminal 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +24 -6
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +27 -3
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/financials.ts +32 -1
- package/src/types/headless.ts +21 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
3
3
|
import type { HostQueryBarItem, HostQueryBarProps } from "../../../ui/host";
|
|
4
|
+
import { withChoiceGroupHeadings } from "../../../components/ui/choice-groups";
|
|
4
5
|
import { WebPopover } from "./popover";
|
|
5
6
|
import { WebMenu } from "./menu";
|
|
6
7
|
import { useHorizontalOverflow } from "../host/overflow-fade";
|
|
@@ -75,15 +76,16 @@ function QueryMenu({ item, onClose }: { item: HostQueryBarItem; onClose: () => v
|
|
|
75
76
|
<WebMenu
|
|
76
77
|
label={item.label}
|
|
77
78
|
selection={item.kind === "multi" ? "multi" : "single"}
|
|
78
|
-
items={item.options.map((option) => ({
|
|
79
|
+
items={withChoiceGroupHeadings(item.options.map((option) => ({
|
|
79
80
|
id: option.value,
|
|
80
81
|
label: option.label,
|
|
82
|
+
group: option.group,
|
|
81
83
|
description: option.description,
|
|
82
84
|
disabled: option.disabled,
|
|
83
85
|
selected: item.kind === "select" ? option.selected : undefined,
|
|
84
86
|
checked: item.kind === "multi" ? option.selected : undefined,
|
|
85
87
|
color: option.color,
|
|
86
|
-
}))}
|
|
88
|
+
})))}
|
|
87
89
|
onSelect={(value) => {
|
|
88
90
|
item.onSelect(value);
|
|
89
91
|
if (item.kind === "select") onClose();
|
|
@@ -5,6 +5,7 @@ import { WEB_CELL_HEIGHT } from "../../../theme/font-scale";
|
|
|
5
5
|
import { Box } from "../../../ui";
|
|
6
6
|
import { MenuPopover } from "../../../components/ui/menu";
|
|
7
7
|
import type { SelectFieldHandle, SelectFieldProps } from "../../../components/ui/select-field";
|
|
8
|
+
import { withChoiceGroupHeadings } from "../../../components/ui/choice-groups";
|
|
8
9
|
import { WebIcon } from "./icons";
|
|
9
10
|
import { isInsideDialogSurface } from "../host/focus-scope";
|
|
10
11
|
|
|
@@ -136,13 +137,14 @@ export function WebSelectField({
|
|
|
136
137
|
minWidth={typeof resolvedWidth === "number" ? Math.max(160, resolvedWidth) : 170}
|
|
137
138
|
items={[
|
|
138
139
|
...(showUnset ? [{ id: UNSET_VALUE, label: "Unset", selected: true, disabled: true }] : []),
|
|
139
|
-
...options.map((option) => ({
|
|
140
|
+
...withChoiceGroupHeadings(options.map((option) => ({
|
|
140
141
|
id: option.value,
|
|
141
142
|
label: option.label,
|
|
143
|
+
group: option.group,
|
|
142
144
|
description: option.description,
|
|
143
145
|
disabled: option.disabled,
|
|
144
146
|
selected: option.value === value,
|
|
145
|
-
})),
|
|
147
|
+
}))),
|
|
146
148
|
]}
|
|
147
149
|
onSelect={(id) => {
|
|
148
150
|
if (id !== UNSET_VALUE && id !== value) onChange(id);
|
|
@@ -168,6 +168,27 @@ const ChartVectors = memo(function ChartVectors({ vectors }: { vectors: readonly
|
|
|
168
168
|
/>
|
|
169
169
|
))
|
|
170
170
|
: [])}
|
|
171
|
+
{vectors.map((shape) => shape.label && shape.points[0]
|
|
172
|
+
? (
|
|
173
|
+
<div
|
|
174
|
+
key={`${shape.id}:label`}
|
|
175
|
+
style={{
|
|
176
|
+
position: "absolute",
|
|
177
|
+
left: `${shape.points[0].x * 100}%`,
|
|
178
|
+
top: `${shape.points[0].y * 100}%`,
|
|
179
|
+
transform: "translate(2px, -100%)",
|
|
180
|
+
color: shape.color,
|
|
181
|
+
fontSize: "var(--gloom-type-caption)",
|
|
182
|
+
lineHeight: 1.2,
|
|
183
|
+
whiteSpace: "nowrap",
|
|
184
|
+
pointerEvents: "none",
|
|
185
|
+
zIndex: 10,
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{shape.label}
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
: null)}
|
|
171
192
|
</>
|
|
172
193
|
);
|
|
173
194
|
});
|
|
@@ -3,7 +3,7 @@ import { access, lstat, readFile, readlink, rename, rm, symlink } from "fs/promi
|
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
4
4
|
import { HOMEBREW_PREFIXES, macCommandFolders } from "../../../../updater/command-names";
|
|
5
5
|
import { logFailure } from "../../../../utils/log-failure";
|
|
6
|
-
import {
|
|
6
|
+
import { DESKTOP_APP_NAME, isDesktopAppIdentifier, LEGACY_DESKTOP_APP_NAME } from "../../shared/app-name";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A Mac install from before the rename is `Gloomberb.app`, and its updater
|
|
@@ -171,7 +171,7 @@ export async function renameLegacyMacApp(options: MacAppNameOptions): Promise<Ma
|
|
|
171
171
|
|
|
172
172
|
const renamed = join(dirname(bundle), APP_BUNDLE);
|
|
173
173
|
const occupied = await exists(renamed);
|
|
174
|
-
if (occupied && await bundleIdentifier(renamed)
|
|
174
|
+
if (occupied && !isDesktopAppIdentifier(await bundleIdentifier(renamed))) return { kind: "skipped", reason: "name-taken" };
|
|
175
175
|
const links = await movableLinks(bundle, options);
|
|
176
176
|
if (!links) return { kind: "skipped", reason: "command-folder-not-writable" };
|
|
177
177
|
|
|
@@ -209,7 +209,7 @@ export async function removeLegacyMacApp(options: MacAppNameOptions): Promise<st
|
|
|
209
209
|
const bundle = runningBundle(options.execPath);
|
|
210
210
|
if (!bundle || basename(bundle) !== APP_BUNDLE) return null;
|
|
211
211
|
const legacy = join(dirname(bundle), LEGACY_APP_BUNDLE);
|
|
212
|
-
if (!(await exists(legacy)) || await bundleIdentifier(legacy)
|
|
212
|
+
if (!(await exists(legacy)) || !isDesktopAppIdentifier(await bundleIdentifier(legacy))) return null;
|
|
213
213
|
if (await managedByHomebrew(options.homebrewPrefixes ?? HOMEBREW_PREFIXES)) return null;
|
|
214
214
|
const links = await movableLinks(legacy, options);
|
|
215
215
|
if (!links) return null;
|
|
@@ -16,11 +16,14 @@ import { startRemoteControlServer, type RemoteControlServer } from "../../../../
|
|
|
16
16
|
import type { RemoteControlRequest, RemoteControlResponse } from "../../../../remote/types";
|
|
17
17
|
import { installCrashReporter } from "../../../../telemetry/crash-reports";
|
|
18
18
|
import { createNodeCrashReporterHost } from "../../../../telemetry/crash-reports-node";
|
|
19
|
+
import { installStallReporter, parseStallEvent, recordStall } from "../../../../telemetry/stall-reports";
|
|
20
|
+
import { createNodeStallReporterHost } from "../../../../telemetry/stall-reports-node";
|
|
19
21
|
import { installUpdateFailureReporter, reviewLastUpdateAttempt } from "../../../../telemetry/update-failures";
|
|
20
22
|
import { createNodeUpdateFailureHost } from "../../../../telemetry/update-failures-node";
|
|
21
23
|
import type { AppConfig } from "../../../../types/config";
|
|
22
24
|
import { linkDesktopCommands } from "../../../../updater/command-names";
|
|
23
25
|
import { debugLog } from "../../../../utils/debug-log";
|
|
26
|
+
import { setStallSink, startMainThreadStallMonitor } from "../../../../utils/main-thread-stall";
|
|
24
27
|
import type { DesktopSharedStateSnapshot } from "../../../../types/desktop-window";
|
|
25
28
|
import type {
|
|
26
29
|
ContextMenuSelectMessage,
|
|
@@ -156,6 +159,10 @@ export class DesktopBackend {
|
|
|
156
159
|
}));
|
|
157
160
|
reviewLastUpdateAttempt();
|
|
158
161
|
void removeUpdateAsides();
|
|
162
|
+
// Desktop has no Debug Log: freezes of this process and of its windows go to the stall log in the data folder.
|
|
163
|
+
installStallReporter(createNodeStallReporterHost({ getDataDir: () => this.config?.dataDir ?? null }));
|
|
164
|
+
setStallSink((event) => recordStall("app", event));
|
|
165
|
+
startMainThreadStallMonitor();
|
|
159
166
|
}
|
|
160
167
|
// The native menu shows the same accelerators the keys use, so a rebind
|
|
161
168
|
// rebuilds it.
|
|
@@ -259,6 +266,10 @@ export class DesktopBackend {
|
|
|
259
266
|
"host.restart": (message) => {
|
|
260
267
|
this.restart(message);
|
|
261
268
|
},
|
|
269
|
+
"runtime.stall": (message) => {
|
|
270
|
+
const event = parseStallEvent(message);
|
|
271
|
+
if (event) recordStall("window", event);
|
|
272
|
+
},
|
|
262
273
|
},
|
|
263
274
|
},
|
|
264
275
|
});
|
|
@@ -564,6 +575,7 @@ export class DesktopBackend {
|
|
|
564
575
|
case "host.notify":
|
|
565
576
|
case "host.showContextMenu":
|
|
566
577
|
case "host.saveBootFrame":
|
|
578
|
+
case "host.saveViewIds":
|
|
567
579
|
case "host.setAppBadge":
|
|
568
580
|
return handleDesktopHostRequest(this, rpc, request);
|
|
569
581
|
case "update.check":
|
|
@@ -13,6 +13,7 @@ import { applyDesktopWindowControl } from "../window/controls";
|
|
|
13
13
|
import { saveTextFileToDownloads } from "../../../../utils/save-text-file";
|
|
14
14
|
import { setAppBadge } from "./app-badge";
|
|
15
15
|
import { desktopBootFramePath, saveDesktopBootFrame } from "../window/boot-frame";
|
|
16
|
+
import { saveDesktopViewIds } from "./view-ids";
|
|
16
17
|
import type { DesktopBackend, DesktopRpc } from "./backend";
|
|
17
18
|
|
|
18
19
|
function normalizeText(value: unknown): string | undefined {
|
|
@@ -162,6 +163,12 @@ export async function handleDesktopHostRequest(
|
|
|
162
163
|
session: backend.config !== null,
|
|
163
164
|
});
|
|
164
165
|
return null;
|
|
166
|
+
case "host.saveViewIds":
|
|
167
|
+
// The main window's view keeps them; its popped-out panes share its storage.
|
|
168
|
+
if (backend.rpcs.getRpcWindowKey(rpc) === MAIN_WINDOW_RPC_KEY && backend.config) {
|
|
169
|
+
saveDesktopViewIds(backend.config.dataDir, request.payload.ids);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
165
172
|
case "host.setAppBadge":
|
|
166
173
|
// One badge for the app, from the main window; a popped-out pane's view
|
|
167
174
|
// has its own chat state and must not overwrite it.
|
|
@@ -24,6 +24,7 @@ import type { CapabilityRegistry } from "../../../../capabilities";
|
|
|
24
24
|
import { telemetryOptedOut } from "../../../../telemetry/crash-reports";
|
|
25
25
|
import { describeNodeOs, readOrCreateInstallId } from "../../../../telemetry/crash-reports-node";
|
|
26
26
|
import type { DesktopBackend, DesktopRpc } from "./backend";
|
|
27
|
+
import { readDesktopViewIds } from "./view-ids";
|
|
27
28
|
|
|
28
29
|
interface DesktopWindowTarget {
|
|
29
30
|
kind: "main" | "detached";
|
|
@@ -94,6 +95,7 @@ function buildInitializationPayload(
|
|
|
94
95
|
homeDir: homedir(),
|
|
95
96
|
optedOut: telemetryOptedOut(process.env),
|
|
96
97
|
},
|
|
98
|
+
viewIds: readDesktopViewIds(config.dataDir),
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, rm } from "fs/promises";
|
|
1
|
+
import { open, readdir, rm } from "fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
3
|
import type Electrobun from "electrobun/bun";
|
|
4
4
|
import type { UpdateStatusEntry } from "electrobun/bun";
|
|
@@ -145,24 +145,101 @@ export function firstTarPath(bytes: Uint8Array): string | null {
|
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* An octal number field of a tar header, read as tar readers do: the digits
|
|
150
|
+
* after any leading spaces, up to the first other character. The checksum is
|
|
151
|
+
* what tells a header from noise.
|
|
152
|
+
*/
|
|
153
|
+
function tarOctal(header: Uint8Array, start: number, length: number): number {
|
|
154
|
+
const digits = /^[\0 ]*([0-7]*)/.exec(new TextDecoder().decode(header.subarray(start, start + length)))?.[1];
|
|
155
|
+
return digits ? Number.parseInt(digits, 8) : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether a header's checksum matches it: its bytes summed with the checksum field as spaces. */
|
|
159
|
+
function tarHeaderChecksumMatches(header: Uint8Array): boolean {
|
|
160
|
+
const stored = tarOctal(header, 148, 8);
|
|
161
|
+
let unsigned = 0;
|
|
162
|
+
let signed = 0;
|
|
163
|
+
for (let index = 0; index < TAR_BLOCK; index += 1) {
|
|
164
|
+
const byte = index >= 148 && index < 156 ? 0x20 : header[index]!;
|
|
165
|
+
unsigned += byte;
|
|
166
|
+
signed += byte > 127 ? byte - 256 : byte;
|
|
167
|
+
}
|
|
168
|
+
return stored === unsigned || stored === signed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether the file at `path` is a whole tar: every header from the first
|
|
173
|
+
* matches its checksum, and the entries reach the end-of-archive block before
|
|
174
|
+
* the file ends. A tar cut off partway (a decompress that failed, an app that
|
|
175
|
+
* quit while writing it) or no tar at all is not. It reads the headers only,
|
|
176
|
+
* so it says nothing about the files' contents.
|
|
177
|
+
*/
|
|
178
|
+
export async function isWholeTar(path: string): Promise<boolean> {
|
|
179
|
+
const file = await open(path, "r").catch(() => null);
|
|
180
|
+
if (!file) return false;
|
|
181
|
+
try {
|
|
182
|
+
const { size } = await file.stat();
|
|
183
|
+
const header = new Uint8Array(TAR_BLOCK);
|
|
184
|
+
for (let offset = 0; offset + TAR_BLOCK <= size;) {
|
|
185
|
+
const { bytesRead } = await file.read(header, 0, TAR_BLOCK, offset);
|
|
186
|
+
if (bytesRead < TAR_BLOCK) return false;
|
|
187
|
+
if (header.every((byte) => byte === 0)) return offset > 0;
|
|
188
|
+
if (!tarHeaderChecksumMatches(header)) return false;
|
|
189
|
+
// Octal: the base-256 sizes past 8 GB are not in an app's archive.
|
|
190
|
+
const entrySize = tarOctal(header, 124, 12);
|
|
191
|
+
offset += TAR_BLOCK + Math.ceil(entrySize / TAR_BLOCK) * TAR_BLOCK;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
} finally {
|
|
197
|
+
await file.close().catch(() => {});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Whether `path` holds a file that is not a whole tar. A missing one is not damaged. */
|
|
202
|
+
async function isDamagedTar(path: string): Promise<boolean> {
|
|
203
|
+
return await Bun.file(path).exists() && !(await isWholeTar(path));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Where Electrobun keeps the update with build hash `hash` once it is downloaded. */
|
|
207
|
+
function stagedArchivePath(extractionFolder: string, hash: string | undefined): string | null {
|
|
208
|
+
return hash ? join(extractionFolder, `${hash}.tar`) : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function removeStagedArchive(path: string): Promise<void> {
|
|
212
|
+
await rm(path, { force: true }).catch(logFailure("desktop-update", "Could not remove a damaged update archive"));
|
|
213
|
+
}
|
|
214
|
+
|
|
148
215
|
/**
|
|
149
216
|
* Removes the unpacked update archives in Electrobun's `self-extraction`
|
|
150
|
-
* folder
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
217
|
+
* folder it must not build on. Electrobun takes `<hash>.tar` there as the
|
|
218
|
+
* update already downloaded without reading it, and a damaged one (part of a
|
|
219
|
+
* tar, from a decompress that failed) then failed to unpack at every install:
|
|
220
|
+
* without it, the download fetches a fresh copy. Also removed are the ones
|
|
221
|
+
* whose app carries its name from before the rename. The one that matters is
|
|
222
|
+
* the Windows archive an install from then updated from: it is packed again
|
|
223
|
+
* with the folder renamed (scripts/write-legacy-desktop-files.ts), so its
|
|
224
|
+
* bytes are not those of the archive the next release's patch is made from,
|
|
225
|
+
* and bspatch would turn it into a broken build. Without it the updater
|
|
155
226
|
* downloads the full archive instead. Resolves to the paths it removed.
|
|
156
227
|
*/
|
|
157
|
-
async function
|
|
228
|
+
async function removeUnusableUpdateArchives(folder: string): Promise<string[]> {
|
|
158
229
|
const removed: string[] = [];
|
|
159
230
|
for (const entry of await readdir(folder).catch(() => [] as string[])) {
|
|
160
231
|
if (!entry.endsWith(".tar")) continue;
|
|
161
232
|
const path = join(folder, entry);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
233
|
+
let reason: string;
|
|
234
|
+
if (!(await isWholeTar(path))) {
|
|
235
|
+
reason = "Could not remove a damaged update archive";
|
|
236
|
+
} else {
|
|
237
|
+
const head = await Bun.file(path).slice(0, 64 * 1024).bytes().catch(() => null);
|
|
238
|
+
const top = head ? firstTarPath(head)?.replace(/^\.\//, "").split("/")[0] : undefined;
|
|
239
|
+
if (top !== LEGACY_DESKTOP_APP_NAME && top !== `${LEGACY_DESKTOP_APP_NAME}.app`) continue;
|
|
240
|
+
reason = "Could not remove an update archive from before the rename";
|
|
241
|
+
}
|
|
242
|
+
const done = await rm(path, { force: true }).then(() => true, logFailure("desktop-update", reason, false));
|
|
166
243
|
if (done) removed.push(path);
|
|
167
244
|
}
|
|
168
245
|
return removed;
|
|
@@ -343,6 +420,12 @@ export function createElectrobunDesktopUpdater(
|
|
|
343
420
|
/** An apply is under way, or finished and the app is quitting. */
|
|
344
421
|
let applyInProgress = false;
|
|
345
422
|
|
|
423
|
+
/** Electrobun's `self-extraction` folder, where the download leaves the update as `<hash>.tar`. */
|
|
424
|
+
async function extractionFolder(): Promise<string | null> {
|
|
425
|
+
const appData = await updater.appDataFolder?.().catch(() => null);
|
|
426
|
+
return appData ? join(appData, "self-extraction") : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
346
429
|
/** The error progress for a failed step, reported to error tracking on the way. */
|
|
347
430
|
function failed(failure: UpdateFailure, error: string, release?: { version?: string; channel?: UpdateChannel } | null): UpdateProgress {
|
|
348
431
|
reportUpdateFailure(failure, {
|
|
@@ -529,8 +612,8 @@ export function createElectrobunDesktopUpdater(
|
|
|
529
612
|
}
|
|
530
613
|
target = result.release;
|
|
531
614
|
|
|
532
|
-
const
|
|
533
|
-
if (
|
|
615
|
+
const extraction = await extractionFolder();
|
|
616
|
+
if (extraction) await removeUnusableUpdateArchives(extraction);
|
|
534
617
|
await updater.downloadUpdate();
|
|
535
618
|
const updateInfo = updater.updateInfo();
|
|
536
619
|
if (updateInfo?.error) {
|
|
@@ -541,6 +624,15 @@ export function createElectrobunDesktopUpdater(
|
|
|
541
624
|
onProgress(failed({ phase: "download", code: "interrupted" }, "Desktop update did not finish downloading.", result.release));
|
|
542
625
|
return;
|
|
543
626
|
}
|
|
627
|
+
// However Electrobun came by it (a patch, the full download), an archive
|
|
628
|
+
// that is not whole is never offered for a restart. It goes, so the retry
|
|
629
|
+
// downloads it again.
|
|
630
|
+
const staged = extraction ? stagedArchivePath(extraction, updateInfo.hash) : null;
|
|
631
|
+
if (staged && await isDamagedTar(staged)) {
|
|
632
|
+
await removeStagedArchive(staged);
|
|
633
|
+
onProgress(failed({ phase: "verify", code: "corrupt" }, "The downloaded update is damaged.", result.release));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
544
636
|
|
|
545
637
|
// Downloaded, not installed: applying relaunches the app, and that waits
|
|
546
638
|
// for the user's explicit request (`apply`).
|
|
@@ -570,9 +662,12 @@ export function createElectrobunDesktopUpdater(
|
|
|
570
662
|
}
|
|
571
663
|
|
|
572
664
|
const target = readyRelease;
|
|
665
|
+
const stagedHash = updater.updateInfo()?.hash;
|
|
573
666
|
applyInProgress = true;
|
|
574
667
|
let relaunching = false;
|
|
575
668
|
let reportedError = false;
|
|
669
|
+
/** The install failed on the archive itself: it does not unpack. */
|
|
670
|
+
let damaged = false;
|
|
576
671
|
const report = (progress: UpdateProgress) => {
|
|
577
672
|
if (progress.phase === "error") reportedError = true;
|
|
578
673
|
onProgress(progress);
|
|
@@ -580,6 +675,7 @@ export function createElectrobunDesktopUpdater(
|
|
|
580
675
|
const installFailed = (error: unknown, message: string) => {
|
|
581
676
|
// Electrobun names the system error in its message ("Failed to replace app: EACCES: ...").
|
|
582
677
|
const code = installFailureCode(classifyUpdateError(error), installKind);
|
|
678
|
+
if (code === "corrupt") damaged = true;
|
|
583
679
|
report(failed({ phase: relaunching ? "relaunch" : "apply", code }, message, target));
|
|
584
680
|
};
|
|
585
681
|
// Kept before Electrobun quits the app, so the next launch can tell whether it took.
|
|
@@ -610,9 +706,14 @@ export function createElectrobunDesktopUpdater(
|
|
|
610
706
|
}
|
|
611
707
|
|
|
612
708
|
// Past this point the app is quitting, so the guard stays up. A failed apply
|
|
613
|
-
// drops the staged release: the retry starts from the download again.
|
|
709
|
+
// drops the staged release: the retry starts from the download again. An
|
|
710
|
+
// archive that does not unpack goes too, or that download would take it as
|
|
711
|
+
// done once more and the install would fail on it again.
|
|
614
712
|
if (!relaunching) {
|
|
615
713
|
readyRelease = null;
|
|
714
|
+
const extraction = damaged ? await extractionFolder() : null;
|
|
715
|
+
const staged = extraction ? stagedArchivePath(extraction, stagedHash) : null;
|
|
716
|
+
if (staged) await removeStagedArchive(staged);
|
|
616
717
|
applyInProgress = false;
|
|
617
718
|
}
|
|
618
719
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { DesktopViewIds } from "../../shared/protocol";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The copy on disk of the ids the desktop view keeps in its WebView storage
|
|
7
|
+
* (view/view-ids.ts), in the data folder beside the install id. That storage
|
|
8
|
+
* belongs to the app's bundle identifier, and this copy is what a build under
|
|
9
|
+
* another identifier starts from.
|
|
10
|
+
*/
|
|
11
|
+
const FILE_NAME = "desktop-view-ids.json";
|
|
12
|
+
const KEY = /^gloomberb\.[a-z0-9.-]{1,64}$/i;
|
|
13
|
+
const MAX_IDS = 16;
|
|
14
|
+
const MAX_VALUE_LENGTH = 4096;
|
|
15
|
+
|
|
16
|
+
function viewIdsPath(dataDir: string): string {
|
|
17
|
+
return join(dataDir, FILE_NAME);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The entries of `raw` shaped like view ids: a storage key of this app's and a short string. */
|
|
21
|
+
function parseViewIds(raw: unknown): DesktopViewIds {
|
|
22
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
23
|
+
const ids: DesktopViewIds = {};
|
|
24
|
+
for (const [key, value] of Object.entries(raw).slice(0, MAX_IDS)) {
|
|
25
|
+
if (KEY.test(key) && typeof value === "string" && value.length > 0 && value.length <= MAX_VALUE_LENGTH) ids[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What the data folder keeps; nothing when the file is missing or unreadable. */
|
|
31
|
+
export function readDesktopViewIds(dataDir: string): DesktopViewIds {
|
|
32
|
+
try {
|
|
33
|
+
return parseViewIds(JSON.parse(readFileSync(viewIdsPath(dataDir), "utf8")));
|
|
34
|
+
} catch {
|
|
35
|
+
// Intentional ignore: no copy yet (a first launch, or a build from before it was kept), so the view keeps what it has.
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Replaces the copy with what the view sent, written whole or not at all. Throws when it cannot, so the view tries again. */
|
|
41
|
+
export function saveDesktopViewIds(dataDir: string, ids: unknown): void {
|
|
42
|
+
const path = viewIdsPath(dataDir);
|
|
43
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(dataDir, { recursive: true });
|
|
46
|
+
writeFileSync(temporary, JSON.stringify(parseViewIds(ids)));
|
|
47
|
+
renameSync(temporary, path);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
rmSync(temporary, { force: true });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -15,3 +15,16 @@ export const LEGACY_DESKTOP_APP_NAME = "Gloomberb";
|
|
|
15
15
|
* to it.
|
|
16
16
|
*/
|
|
17
17
|
export const DESKTOP_APP_IDENTIFIER = "com.vincelwt.gloomberb";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The identifiers a build of this app carries: the one above, and the one a
|
|
21
|
+
* later release moves to. Until every install has moved, either one can be
|
|
22
|
+
* on disk (an app bundle, an install the script or Homebrew replaces), so
|
|
23
|
+
* anything that asks whether a copy is this app accepts both.
|
|
24
|
+
*/
|
|
25
|
+
export const DESKTOP_APP_IDENTIFIERS: readonly string[] = [DESKTOP_APP_IDENTIFIER, "sh.gloom.desktop"];
|
|
26
|
+
|
|
27
|
+
/** Whether `identifier` is one a build of this app carries. */
|
|
28
|
+
export function isDesktopAppIdentifier(identifier: string | null | undefined): boolean {
|
|
29
|
+
return !!identifier && DESKTOP_APP_IDENTIFIERS.includes(identifier);
|
|
30
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { CapabilityManifest } from "../../../capabilities";
|
|
|
10
10
|
import type { PluginOperationResult, PluginPin } from "../../../plugins/marketplace/store";
|
|
11
11
|
import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
|
|
12
12
|
import type { HttpProxyRequestEnvelope, HttpProxyResponseEnvelope } from "../../../utils/http-proxy-response";
|
|
13
|
+
import type { StallEvent } from "../../../utils/main-thread-stall";
|
|
13
14
|
import type { BootFrame } from "../../dom/boot-frame-page";
|
|
14
15
|
|
|
15
16
|
export const ELECTROBUN_CONTEXT_MENU_ACTION = "gloom.context-menu.select";
|
|
@@ -32,8 +33,13 @@ export interface ElectrobunBackendInit {
|
|
|
32
33
|
pluginSeedNotice?: { body: string; type: "error" | "info" };
|
|
33
34
|
/** What the view needs to send crash reports and usage counts the same way the Bun process would. */
|
|
34
35
|
telemetry: ElectrobunTelemetryInit;
|
|
36
|
+
/** The copy on disk of the ids the view keeps in its own storage (view/view-ids.ts). */
|
|
37
|
+
viewIds: DesktopViewIds;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
/** Ids the view keeps in its WebView storage, by storage key. */
|
|
41
|
+
export type DesktopViewIds = Record<string, string>;
|
|
42
|
+
|
|
37
43
|
interface ElectrobunTelemetryInit {
|
|
38
44
|
/** Random per-install id from `<dataDir>/install-id`; null when the file could not be written. */
|
|
39
45
|
installId: string | null;
|
|
@@ -222,6 +228,8 @@ interface DesktopBackendRequestMap {
|
|
|
222
228
|
"host.showContextMenu": { request: { menu: DesktopContextMenuItem[] }; response: boolean };
|
|
223
229
|
/** The layout the main window draws at the next launch before its script loads (dom/boot-frame.ts). Only the main window's is kept. */
|
|
224
230
|
"host.saveBootFrame": { request: { frame: BootFrame }; response: null };
|
|
231
|
+
/** The ids the view keeps in its storage, for the copy on disk. Only the main window's are kept. */
|
|
232
|
+
"host.saveViewIds": { request: { ids: DesktopViewIds }; response: null };
|
|
225
233
|
/** The unread count for the Dock icon badge: `""` (none), `"1"` to `"99"` or `"99+"`. Only the main window's is applied. */
|
|
226
234
|
"host.setAppBadge": { request: { label: string }; response: null };
|
|
227
235
|
/** `channel` absent (an older view) means stable. */
|
|
@@ -337,6 +345,8 @@ export interface ElectrobunDesktopRpcSchema {
|
|
|
337
345
|
};
|
|
338
346
|
messages: {
|
|
339
347
|
"host.restart": DesktopRestartMessage;
|
|
348
|
+
/** A window froze; the Bun process keeps the stall log and its report. */
|
|
349
|
+
"runtime.stall": StallEvent;
|
|
340
350
|
};
|
|
341
351
|
};
|
|
342
352
|
webview: {
|
|
@@ -17,6 +17,7 @@ import { subscribeCapability, type CapabilitySubscriptionOptions } from "./capab
|
|
|
17
17
|
import { RpcTimeoutError } from "../../../utils/rpc-timeout-error";
|
|
18
18
|
import { nameRpcTimeout, RPC_MAX_REQUEST_TIME_MS } from "./rpc-timeout";
|
|
19
19
|
import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
|
|
20
|
+
import type { StallEvent } from "../../../utils/main-thread-stall";
|
|
20
21
|
|
|
21
22
|
type BackendMessages = ElectrobunDesktopRpcSchema["webview"]["messages"];
|
|
22
23
|
type BackendMessageName = keyof BackendMessages;
|
|
@@ -153,6 +154,11 @@ export function requestElectrobunRestart(message: DesktopRestartMessage = {}): v
|
|
|
153
154
|
rpc.send["host.restart"](message);
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
/** The Bun process keeps the stall log and sends its report. */
|
|
158
|
+
export function sendElectrobunStall(event: StallEvent): void {
|
|
159
|
+
rpc.send["runtime.stall"](event);
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
export function getElectrobunBackendInitSnapshot(): ElectrobunBackendInit | null {
|
|
157
163
|
return initSnapshot;
|
|
158
164
|
}
|
|
@@ -5,12 +5,14 @@ import { App } from "../../../app";
|
|
|
5
5
|
import { applyLanguageFromConfig } from "../../../i18n";
|
|
6
6
|
import { debugLog } from "../../../utils/debug-log";
|
|
7
7
|
import { capturePaneImage } from "../../../utils/dom-screenshot";
|
|
8
|
+
import { setStallSink } from "../../../utils/main-thread-stall";
|
|
8
9
|
import { measurePerfAsync } from "../../../utils/perf-marks";
|
|
9
10
|
import {
|
|
10
11
|
backendRequest,
|
|
11
12
|
initElectrobunBackend,
|
|
12
13
|
onBackendMessage,
|
|
13
14
|
replaceElectrobunCapabilityManifests,
|
|
15
|
+
sendElectrobunStall,
|
|
14
16
|
setElectrobunRemoteRequestHandler,
|
|
15
17
|
} from "./backend-rpc";
|
|
16
18
|
import { installElectrobunCapabilityStreamClient } from "./capability-stream-client";
|
|
@@ -49,10 +51,13 @@ import { loadOfficialPluginIds } from "../../../plugins/marketplace/feed";
|
|
|
49
51
|
import { setPluginSeedNotice } from "../../../plugins/seed-notice";
|
|
50
52
|
import { crashReportsEnabled, installCrashReporter, reportCrash } from "../../../telemetry/crash-reports";
|
|
51
53
|
import { browserDoNotTrack, installWindowCrashListeners } from "../../../telemetry/crash-reports-dom";
|
|
54
|
+
import { forwardAttendedStalls } from "../../../telemetry/stall-reports-dom";
|
|
52
55
|
import { currentTelemetryConfig } from "../../../telemetry/live-config";
|
|
53
56
|
import { attentionCountsEnabled, installAttentionCounter } from "../../../telemetry/attention-counts";
|
|
54
57
|
import { installUsageCounter, usageCountsEnabled } from "../../../telemetry/usage-counts";
|
|
55
58
|
import { installWindowUsageFlush } from "../../../telemetry/usage-counts-dom";
|
|
59
|
+
import { cloudSyncController } from "../../../sync/controller";
|
|
60
|
+
import { createViewIdMirror, restoreViewIds, viewIdStorage } from "./view-ids";
|
|
56
61
|
|
|
57
62
|
// Declared here rather than sniffed: the desktop view and the hosted browser
|
|
58
63
|
// app are both browser contexts but differ in what plugins may do.
|
|
@@ -156,6 +161,8 @@ async function boot() {
|
|
|
156
161
|
officialPluginIds: loadOfficialPluginIds,
|
|
157
162
|
});
|
|
158
163
|
installAttentionCounter(() => !init.telemetry.optedOut && !browserDoNotTrack() && attentionCountsEnabled(currentTelemetryConfig(init.config)));
|
|
164
|
+
// Desktop has no Debug Log: this window's freezes go to the Bun process, which keeps the stall log.
|
|
165
|
+
setStallSink(forwardAttendedStalls(sendElectrobunStall).sink);
|
|
159
166
|
installElectrobunCapabilityStreamClient();
|
|
160
167
|
installFocusScopeRelease();
|
|
161
168
|
installElectrobunWindowFullscreenTracking();
|
|
@@ -173,10 +180,26 @@ async function boot() {
|
|
|
173
180
|
const desktopApplicationMenuBridge: DesktopApplicationMenuBridge = {
|
|
174
181
|
subscribe: (listener) => onBackendMessage("application-menu.select", ({ command }) => listener(command)),
|
|
175
182
|
};
|
|
183
|
+
// Before anything reads them: ids this view's storage lacks come back from
|
|
184
|
+
// the copy on disk, and the main window keeps that copy up to date. The sync
|
|
185
|
+
// id is made at the first push, and a gloom.sh link can bring a visitor id.
|
|
186
|
+
const viewStorage = viewIdStorage();
|
|
187
|
+
if (viewStorage) restoreViewIds(viewStorage, init.viewIds);
|
|
188
|
+
const mirrorViewIds = viewStorage && init.windowKind === "main"
|
|
189
|
+
? createViewIdMirror(viewStorage, init.viewIds, (ids) => backendRequest("host.saveViewIds", { ids }))
|
|
190
|
+
: () => {};
|
|
191
|
+
mirrorViewIds();
|
|
192
|
+
cloudSyncController.subscribe(mirrorViewIds);
|
|
176
193
|
initializeDesktopResearchActivity();
|
|
177
|
-
const
|
|
194
|
+
const observedDeepLinks = observeDesktopDeepLinks({
|
|
178
195
|
subscribe: (listener) => onBackendMessage("desktop.deepLink", listener),
|
|
179
196
|
});
|
|
197
|
+
const desktopDeepLinkBridge: typeof observedDeepLinks = {
|
|
198
|
+
subscribe: (listener) => observedDeepLinks.subscribe((deeplink) => {
|
|
199
|
+
listener(deeplink);
|
|
200
|
+
mirrorViewIds();
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
180
203
|
const webUiHost = createWebUiHost(init.desktopPlatform);
|
|
181
204
|
// Compiled by the Bun process, which owns the filesystem. A failure here must
|
|
182
205
|
// not stop the app from starting: the marketplace reports broken plugins, and
|
|
@@ -10,7 +10,8 @@ interface BackendNews {
|
|
|
10
10
|
getNewsPage(query: NewsQuery): Promise<NewsPage>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "getCloudNewsStory" | "searchCloudNews"
|
|
13
|
+
type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "getCloudNewsStory" | "searchCloudNews">
|
|
14
|
+
& Partial<Pick<typeof apiClient, "hasSessionCredential">>;
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The view's only news source: the desktop view runs without capability
|
|
@@ -32,6 +33,7 @@ export function createDesktopNewsSource(backend: BackendNews, client: CloudNewsC
|
|
|
32
33
|
return {
|
|
33
34
|
articles: cloudNewsReadArticles(read, query.ticker),
|
|
34
35
|
nextCursor: read.nextCursor ?? null,
|
|
36
|
+
access: read.access ?? null,
|
|
35
37
|
};
|
|
36
38
|
} catch {
|
|
37
39
|
// The aggregate page keeps RSS failures beside any usable stories.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TickerMetadata, TickerRecord } from "../../../../types/ticker";
|
|
2
2
|
import { backendRequest } from "../backend-rpc";
|
|
3
|
+
import { runTickerWrite } from "../../../../data/ticker-writes";
|
|
3
4
|
|
|
4
5
|
export class RemoteTickerRepository {
|
|
5
6
|
async loadAllTickers(): Promise<TickerRecord[]> {
|
|
@@ -10,17 +11,23 @@ export class RemoteTickerRepository {
|
|
|
10
11
|
return backendRequest("ticker.load", { symbol });
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
saveTicker(ticker: TickerRecord): Promise<void> {
|
|
15
|
+
return runTickerWrite(this, ticker.metadata.ticker, async () => {
|
|
16
|
+
await backendRequest("ticker.save", { ticker });
|
|
17
|
+
});
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
|
|
18
21
|
const ticker: TickerRecord = { metadata };
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
return runTickerWrite(this, metadata.ticker, async () => {
|
|
23
|
+
await backendRequest("ticker.save", { ticker });
|
|
24
|
+
return ticker;
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
deleteTicker(symbol: string): Promise<void> {
|
|
29
|
+
return runTickerWrite(this, symbol, async () => {
|
|
30
|
+
await backendRequest("ticker.delete", { symbol });
|
|
31
|
+
});
|
|
25
32
|
}
|
|
26
33
|
}
|