gloomberb 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- 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/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- 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 +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- 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/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- 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 +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- 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 +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -36,10 +36,12 @@ import {
|
|
|
36
36
|
FILTER_CYCLE,
|
|
37
37
|
attachEconCalendarPersistence,
|
|
38
38
|
actualColor,
|
|
39
|
+
awaitsConsensus,
|
|
39
40
|
calendarDisplayRows,
|
|
40
41
|
CALENDAR_TIME_ZONE_LABEL,
|
|
41
42
|
dateKey,
|
|
42
43
|
dayLabel,
|
|
44
|
+
econEventName,
|
|
43
45
|
filteredEmptyTitle,
|
|
44
46
|
formatStaleness,
|
|
45
47
|
getCalendarCache,
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
eventCountry,
|
|
51
53
|
matchesCountry,
|
|
52
54
|
matchesImpact,
|
|
55
|
+
NO_CONSENSUS_NOTE,
|
|
53
56
|
noScheduledReleasesMessage,
|
|
54
57
|
parseCountryFilter,
|
|
55
58
|
resetEconCalendarPersistence,
|
|
@@ -322,7 +325,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
322
325
|
: formatCountdown(nextEvent.date.getTime() - now);
|
|
323
326
|
// The countdown changes every tick, so it is footer status, not query bar
|
|
324
327
|
// context; the footer ellipsizes a long event name.
|
|
325
|
-
const nextText = nextEvent && nextCountdown ? `next ${nextEvent
|
|
328
|
+
const nextText = nextEvent && nextCountdown ? `next ${econEventName(nextEvent)} ${nextCountdown}` : null;
|
|
326
329
|
// The cached first paint is being replaced; its age only counts once a load has answered.
|
|
327
330
|
const showStale = stale && (!loading || calendar.updatedAt !== null);
|
|
328
331
|
// An empty list says it in its empty state instead.
|
|
@@ -330,6 +333,9 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
330
333
|
// What the High filter, where the calendar opens, leaves out, and the key that lists it (Shift+F steps back to All).
|
|
331
334
|
const hiddenCount = useMemo(() => lowerImpactHidden(events, impactFilter, countryFilter), [countryFilter, events, impactFilter]);
|
|
332
335
|
const hiddenStatus = hiddenCount > 0 && !detailEvent ? `${hiddenCount} lower-impact hidden` : null;
|
|
336
|
+
// The dash under Forecast on the row in hand, while its consensus is not in yet.
|
|
337
|
+
const focusedEvent = detailEvent ?? filtered[selectedIdx];
|
|
338
|
+
const noConsensus = !!focusedEvent && awaitsConsensus(focusedEvent);
|
|
333
339
|
const calendarStatus = useMemo<PaneFooterSegment[]>(() => [
|
|
334
340
|
...(hiddenStatus ? [{
|
|
335
341
|
id: "hidden-impact",
|
|
@@ -341,8 +347,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
341
347
|
...(nextText && !detailEvent ? [{ id: "next", parts: [{ text: nextText, tone: "muted" as const }] }] : []),
|
|
342
348
|
...(listedAfterStatus ? [{ id: "listed-after", parts: [{ text: listedAfterStatus, tone: "warning" as const }] }] : []),
|
|
343
349
|
...(showStale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
|
|
350
|
+
// Behind the warnings, so a narrow footer gives it up before them, and ahead of the update time, which goes first.
|
|
351
|
+
...(noConsensus ? [{ id: "no-consensus", parts: [{ text: NO_CONSENSUS_NOTE, tone: "muted" as const }],
|
|
352
|
+
variants: [[{ text: "no consensus yet", tone: "muted" as const }]] }] : []),
|
|
344
353
|
...(staleness ? [{ id: "updated", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
|
|
345
|
-
], [detailEvent, hiddenStatus, listedAfterStatus, nextText, showStale, staleness]);
|
|
354
|
+
], [detailEvent, hiddenStatus, listedAfterStatus, nextText, noConsensus, showStale, staleness]);
|
|
346
355
|
// A report of the pane (`gloom fn ECO`) says it too.
|
|
347
356
|
const listedThrough = listedEnd ? dateKey(listedEnd) : null;
|
|
348
357
|
useRemoteUiNode(listedAfter && listedThrough ? {
|
|
@@ -433,7 +442,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
433
442
|
return withCountryFlag({ text: country, color: colors.textMuted }, country, flagged);
|
|
434
443
|
}
|
|
435
444
|
case "event":
|
|
436
|
-
return { text: ev
|
|
445
|
+
return { text: econEventName(ev), color: colors.text };
|
|
437
446
|
case "actual":
|
|
438
447
|
return {
|
|
439
448
|
text: ev.actual ?? "—",
|
|
@@ -480,7 +489,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
480
489
|
focused={focused}
|
|
481
490
|
detailOpen={!!detailEvent}
|
|
482
491
|
onBack={() => setOpenKey(null)}
|
|
483
|
-
detailTitle={detailEvent
|
|
492
|
+
detailTitle={detailEvent ? econEventName(detailEvent) : undefined}
|
|
484
493
|
detailContent={detailContent}
|
|
485
494
|
rootWidth={width}
|
|
486
495
|
rootHeight={Math.max(1, height - 1)}
|
|
@@ -576,7 +585,7 @@ export const economicCalendarModule: PluginModule = {
|
|
|
576
585
|
description: "Upcoming economic events, releases, and indicators.",
|
|
577
586
|
// "econ" stays a keyword so the old ECON prefix still finds this pane.
|
|
578
587
|
keywords: ["econ", "economic", "calendar", "events", "macro", "releases", "fed", "cpi", "gdp", "rate decision", "ipca",
|
|
579
|
-
"brazil inflation", "pmi", "ism", "purchasing managers", "flash pmi"],
|
|
588
|
+
"brazil inflation", "pmi", "ism", "purchasing managers", "flash pmi", "jobs report", "nfp", "payrolls", "nonfarm"],
|
|
580
589
|
// ECO MX,BR opens the calendar on those countries.
|
|
581
590
|
shortcut: {
|
|
582
591
|
prefix: "ECO",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
2
2
|
import {
|
|
3
|
+
econEventName,
|
|
3
4
|
eventCountry,
|
|
4
5
|
getCalendarCache,
|
|
5
6
|
landedHighImpactPrints,
|
|
@@ -57,7 +58,8 @@ const DEFAULT_DEPS: PrintWatchDeps = {
|
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
function printBody(event: EconEvent): string {
|
|
60
|
-
|
|
61
|
+
const name = econEventName(event);
|
|
62
|
+
return event.forecast ? `${name} ${event.actual} vs ${event.forecast}` : `${name} ${event.actual}`;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
type PrintWatchContext = Pick<GloomPluginContext, "notify" | "showPane">;
|
|
@@ -101,6 +101,17 @@ function bundleFrom(unaligned: StatBuild[], errors: string[]): StatsBundle {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The bundle's errors less the refresh failures of statistics the copy an
|
|
106
|
+
* earlier read saved answered for: those say when that copy is from instead.
|
|
107
|
+
*/
|
|
108
|
+
export function unansweredErrors(bundle: StatsBundle): string[] {
|
|
109
|
+
const answered = new Set(bundle.builds.flatMap(({ stat, cache }) => (
|
|
110
|
+
cache?.source === "stale-fallback" && cache.refreshError ? [`${stat.seriesId}: ${cache.refreshError}`] : []
|
|
111
|
+
)));
|
|
112
|
+
return bundle.errors.filter((error) => !answered.has(error));
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
/** Builds whatever the on-disk cache can already answer, for an instant first paint. */
|
|
105
116
|
export function getCachedStatsBundle(stats: readonly StatDef[] = STATS): StatsBundle | null {
|
|
106
117
|
const builds: StatBuild[] = [];
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
HeadlessPaneDefinition,
|
|
4
4
|
HeadlessPaneEntry,
|
|
5
5
|
} from "../../../types/plugin";
|
|
6
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
6
7
|
import { fredSourceLabel } from "../../../sources/gloomberb-cloud/fred-series";
|
|
7
8
|
import { formatBasisPoints, toBasisPoints } from "../../../utils/basis-points";
|
|
8
9
|
import { formatNumber } from "../../../utils/format";
|
|
@@ -10,6 +11,7 @@ import type { StatDef } from "./defs";
|
|
|
10
11
|
import {
|
|
11
12
|
createStatSeriesLoader,
|
|
12
13
|
loadStatsBundle,
|
|
14
|
+
unansweredErrors,
|
|
13
15
|
type StatsBundle,
|
|
14
16
|
} from "./client";
|
|
15
17
|
import { historyStartNotice } from "../shared/history-span";
|
|
@@ -84,6 +86,11 @@ function detailEntries(view: StatViewModel, cutoffShown: boolean, text: boolean)
|
|
|
84
86
|
];
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/** A statistic answered from the copy saved by an earlier read, its refresh having failed, says so in its row. */
|
|
90
|
+
function savedCopyRow<T extends object>(view: StatViewModel, row: T): T {
|
|
91
|
+
return view.cacheSource === "stale-fallback" && view.fetchedAt != null ? servedFromSavedCopy(row, view.fetchedAt) : row;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
/** The window the %ile column counts over, in words. */
|
|
88
95
|
const PERCENTILE_WINDOW: Record<StatRangeId, string> = {
|
|
89
96
|
"5Y": "the last 5 years",
|
|
@@ -163,7 +170,7 @@ export function projectStatsHeadlessBundle(
|
|
|
163
170
|
textColumns: columns(true),
|
|
164
171
|
// `as_of` is the release; a figure with none stated is as of its own day when it is observed daily or weekly, never its period's first day.
|
|
165
172
|
rowAsOfKey: "exportAsOf",
|
|
166
|
-
rows: categoryViews.map((view) => ({
|
|
173
|
+
rows: categoryViews.map((view) => savedCopyRow(view, {
|
|
167
174
|
id: view.stat.id,
|
|
168
175
|
indicator: view.stat.shortLabel,
|
|
169
176
|
latest: view.latest.value,
|
|
@@ -208,7 +215,8 @@ export function projectStatsHeadlessBundle(
|
|
|
208
215
|
...exportDating(selected),
|
|
209
216
|
}] : []),
|
|
210
217
|
],
|
|
211
|
-
|
|
218
|
+
// A statistic answered from its saved copy says so in the source line, not as an error.
|
|
219
|
+
errors: unansweredErrors(bundle),
|
|
212
220
|
notes: [
|
|
213
221
|
`%ile is the share of past readings at or below the latest one, over ${PERCENTILE_WINDOW[range]}.`
|
|
214
222
|
+ (range === "ALL" ? "" : " Full history: --range ALL."),
|
|
@@ -12,8 +12,9 @@ import type { PaneProps } from "../../../types/plugin";
|
|
|
12
12
|
import { formatNumber } from "../../../utils/format";
|
|
13
13
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
14
14
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
15
|
+
import { savedCopyFooterSegment } from "../../../components/layout/pane/footer";
|
|
15
16
|
import { SeriesListDetail, useSeriesList } from "../shared/series-list-detail";
|
|
16
|
-
import { getCachedStatsBundle, loadStatsBundle } from "./client";
|
|
17
|
+
import { getCachedStatsBundle, loadStatsBundle, unansweredErrors } from "./client";
|
|
17
18
|
import { categoryLabel, changeColor, type StatCategoryId } from "./defs";
|
|
18
19
|
import { StatDetail } from "./detail";
|
|
19
20
|
import { DEFAULT_STAT_ID } from "./stats";
|
|
@@ -145,7 +146,7 @@ export function EconStatisticsPane({ focused, width, height }: PaneProps) {
|
|
|
145
146
|
const { selected, searchFocused } = list;
|
|
146
147
|
const rows = useMemo(() => withCategoryHeaders(list.visible), [list.visible]);
|
|
147
148
|
|
|
148
|
-
const error = resource.error ?? bundle
|
|
149
|
+
const error = resource.error ?? (bundle ? unansweredErrors(bundle)[0] : null) ?? null;
|
|
149
150
|
const footerInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
150
151
|
if (!selected) return [];
|
|
151
152
|
// A Treasury yield names when in the day it was taken, as GC does, so it is not read against ^TNX's earlier close.
|
|
@@ -153,11 +154,14 @@ export function EconStatisticsPane({ focused, width, height }: PaneProps) {
|
|
|
153
154
|
const info: PaneFooterSegment[] = [
|
|
154
155
|
{ id: "as-of", parts: [{ text: `reading for ${cutoff ? selected.reading : selected.readingReleased}${cutoff}`, tone: "muted" }] },
|
|
155
156
|
];
|
|
156
|
-
// The cached first paint is being replaced; its age only counts once that load fails
|
|
157
|
+
// The cached first paint is being replaced; its age only counts once that load fails, and then it is the
|
|
158
|
+
// copy an earlier read saved, which says when that was.
|
|
157
159
|
const seeding = resource.loading && resource.updatedAt === null && !selected.refreshError;
|
|
158
|
-
|
|
160
|
+
const savedAt = selected.cacheSource === "stale-fallback" ? selected.fetchedAt : null;
|
|
161
|
+
if (selected.observationStale || (selected.cacheStale && !seeding && savedAt == null)) {
|
|
159
162
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
160
163
|
}
|
|
164
|
+
if (savedAt != null) info.push(savedCopyFooterSegment("saved-copy", savedAt));
|
|
161
165
|
return info;
|
|
162
166
|
}, [resource.loading, resource.updatedAt, selected]);
|
|
163
167
|
|
|
@@ -144,6 +144,9 @@ export const STATS: readonly StatDef[] = [
|
|
|
144
144
|
reference: { value: 0, label: "flat" },
|
|
145
145
|
staleAfterMs: MONTHLY_EARLY,
|
|
146
146
|
note: "Jobs added or lost last month, the single most watched release.",
|
|
147
|
+
// The jobs report as it is called: `ECST nfp` opens it.
|
|
148
|
+
aliases: ["nfp", "nonfarm", "nonfarm payrolls", "jobs report"],
|
|
149
|
+
keywords: ["jobs report", "nfp", "nonfarm"],
|
|
147
150
|
limit: 900,
|
|
148
151
|
}),
|
|
149
152
|
stat({
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type CloudProxyStatementListPayload,
|
|
4
4
|
type CloudProxyStatementPayload,
|
|
5
5
|
} from "../../../api-client";
|
|
6
|
-
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
6
|
+
import { ApiRequestError, isPermanentClientError, isTransientFailure } from "../../../api-client/errors";
|
|
7
7
|
import type { IssuerListingParams } from "../../../api-client/paths";
|
|
8
8
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
9
9
|
import { secListingKey } from "../../../utils/sec";
|
|
@@ -46,6 +46,8 @@ interface ProxyResult<T> {
|
|
|
46
46
|
/** Null is an explicit 404, not a transient failure or access denial. */
|
|
47
47
|
data: T | null;
|
|
48
48
|
fetchedAt: number | null;
|
|
49
|
+
/** Its refresh failed and the copy saved at this time (ms) answered instead. */
|
|
50
|
+
savedAt?: number;
|
|
49
51
|
refreshError?: string;
|
|
50
52
|
}
|
|
51
53
|
type ActiveRequest<T> = { store: PluginPersistence | null; promise: Promise<ProxyResult<T>> };
|
|
@@ -105,11 +107,14 @@ function loadCached<T>(
|
|
|
105
107
|
}
|
|
106
108
|
throw error;
|
|
107
109
|
}
|
|
110
|
+
// A saved copy answers only for a failure asking again may get past, not for a reply that fails validation.
|
|
111
|
+
if (!isTransientFailure(error)) throw error;
|
|
108
112
|
const fallback = store?.getResource<T>(kind, key, { ...options, allowExpired: true });
|
|
109
113
|
if (!fallback || tierOf(fallback.value) !== tier) throw error;
|
|
110
114
|
return {
|
|
111
115
|
data: fallback.value,
|
|
112
116
|
fetchedAt: fallback.fetchedAt,
|
|
117
|
+
savedAt: fallback.fetchedAt,
|
|
113
118
|
refreshError: (error instanceof Error ? error.message : String(error)).trim() || "Request failed",
|
|
114
119
|
};
|
|
115
120
|
}).finally(() => { if (active.get(activeKey) === request) active.delete(activeKey); });
|
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
DataTableView,
|
|
9
9
|
EmptyState, FigureList, PaneStatusBody, Prose, QueryBar, READING_WIDTH, SectionHeading,
|
|
10
10
|
usePaneFooter,
|
|
11
|
-
usePaneNoticeFooter,
|
|
12
11
|
type DataTableCell,
|
|
13
12
|
type DataTableColumn,
|
|
14
13
|
type PaneFooterSegment,
|
|
15
14
|
type PaneHint,
|
|
16
15
|
} from "../../../components";
|
|
16
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
17
17
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
18
18
|
import { usePaneStateValue } from "../../../state/app/context";
|
|
19
19
|
import { colors, getChartIndicatorColor } from "../../../theme/colors";
|
|
@@ -45,13 +45,6 @@ import {
|
|
|
45
45
|
|
|
46
46
|
export const EXECUTIVES_PANE_ID = "executives";
|
|
47
47
|
|
|
48
|
-
function retainedDataNotice(subject: string, error: string, fetchedAt: number | null): string {
|
|
49
|
-
const retrieved = new Date(fetchedAt ?? Number.NaN);
|
|
50
|
-
const age = Number.isFinite(retrieved.getTime())
|
|
51
|
-
? `retrieved ${retrieved.toISOString()}` : "with unavailable retrieval time";
|
|
52
|
-
return `${subject} refresh failed: ${error}. Retained data ${age}.`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
48
|
const PAY_PARTS: Array<{ key: keyof CloudExecutiveRowPayload; label: string }> =
|
|
56
49
|
[
|
|
57
50
|
{ key: "salary", label: "Salary" },
|
|
@@ -301,18 +294,10 @@ function ExecutiveResearch({ ticker, listing, tier, focused, width, nested, guar
|
|
|
301
294
|
// The free preview: each executive's total pay; the breakdown and earlier years are Pro.
|
|
302
295
|
const preview = statement?.access === "preview" || list.data?.data?.access === "preview";
|
|
303
296
|
const loading = list.loading || detail.loading;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
notices: [
|
|
309
|
-
list.data?.data && listError
|
|
310
|
-
? retainedDataNotice("Proxy list", listError, list.data.fetchedAt) : null,
|
|
311
|
-
detail.data?.data && statementError
|
|
312
|
-
? retainedDataNotice(`${year} proxy`, statementError, detail.data.fetchedAt) : null,
|
|
313
|
-
].filter((notice): notice is string => notice !== null),
|
|
314
|
-
focused,
|
|
315
|
-
});
|
|
297
|
+
// A copy saved earlier answering a failed refresh says when it was saved, in the footer every pane uses;
|
|
298
|
+
// a failed refresh with only what is on screen says why. Without anything shown, the body says it.
|
|
299
|
+
const listError = list.data?.data ? list.error ?? (list.data.savedAt == null ? list.data.refreshError : null) ?? null : null;
|
|
300
|
+
const statementError = detail.data?.data ? detail.error ?? (detail.data.savedAt == null ? detail.data.refreshError : null) ?? null : null;
|
|
316
301
|
const refresh = useCallback(() => {
|
|
317
302
|
void list.reload();
|
|
318
303
|
if (year !== null) void detail.reload();
|
|
@@ -338,6 +323,9 @@ function ExecutiveResearch({ ticker, listing, tier, focused, width, nested, guar
|
|
|
338
323
|
const info: PaneFooterSegment[] = [];
|
|
339
324
|
if (loading)
|
|
340
325
|
info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
|
|
326
|
+
if (listError) info.push({ id: "list-error", parts: [{ text: `Proxy list: ${listError}`, tone: "warning" }] });
|
|
327
|
+
if (statementError) info.push({ id: "statement-error", parts: [{ text: `${year} proxy: ${statementError}`, tone: "warning" }] });
|
|
328
|
+
info.push(...savedCopyFooterInfo([list.data?.data ? list.data.savedAt : null, statement ? detail.data?.savedAt : null]));
|
|
341
329
|
if (statement) {
|
|
342
330
|
info.push({
|
|
343
331
|
id: "filed",
|
|
@@ -351,7 +339,7 @@ function ExecutiveResearch({ ticker, listing, tier, focused, width, nested, guar
|
|
|
351
339
|
if (yearHint) hints.push(yearHint);
|
|
352
340
|
if (preview) hints.push({ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: openUpgrade });
|
|
353
341
|
return { info, hints };
|
|
354
|
-
}, [loading, statement, openFiling, yearHint, preview, openUpgrade]);
|
|
342
|
+
}, [loading, listError, statementError, year, list.data, detail.data, statement, openFiling, yearHint, preview, openUpgrade]);
|
|
355
343
|
|
|
356
344
|
const figures = useMemo(
|
|
357
345
|
() => (statement ? figuresOf(statement) : []),
|
|
@@ -35,6 +35,10 @@ export type FearGreedCacheEntry = {
|
|
|
35
35
|
export interface FearGreedLoadResult extends FearGreedCacheEntry {
|
|
36
36
|
/** Set when the network failed and cached data was served instead. */
|
|
37
37
|
refreshError?: string;
|
|
38
|
+
/** `stale-fallback` when a saved copy answered a failed refresh. */
|
|
39
|
+
source?: "cache" | "network" | "stale-fallback";
|
|
40
|
+
/** The failure a `stale-fallback` copy answered. */
|
|
41
|
+
error?: unknown;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
const cache = createPluginCache<FearGreedData, PersistedFearGreedData>({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SeriesDefinition, SeriesLoadResult, SeriesPoint } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { savedCopyForTransientFailure } from "../../../data/plugin-cache";
|
|
3
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
4
|
import { loadFearGreed } from "./cache";
|
|
4
5
|
import type { FearGreedData } from "./data";
|
|
5
6
|
|
|
@@ -30,9 +31,10 @@ function series(id: string, name: string, unit: string, read: Reading, tags: rea
|
|
|
30
31
|
tags: ["fear and greed", ...tags],
|
|
31
32
|
refreshMs: REFRESH_MS,
|
|
32
33
|
load: async (): Promise<SeriesLoadResult> => {
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
return { points: read(result.data),
|
|
34
|
+
// A saved copy answers only a failure asking again may get past; a refusal is the chart's error.
|
|
35
|
+
const result = savedCopyForTransientFailure(await loadFearGreed());
|
|
36
|
+
return { points: read(result.data),
|
|
37
|
+
...cachedSeriesNote({ ...result, ...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}) }) };
|
|
36
38
|
},
|
|
37
39
|
};
|
|
38
40
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolvePlanAccess } from "../../../api-client/plan-rules";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import { apiClient } from "../../../api-client";
|
|
3
4
|
import type { GpuBoardPayload, GpuEventsPayload, GpuHistoryPayload, GpuHistoryQuery, GpuObservation } from "../../../api-client/gpu";
|
|
4
5
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
@@ -94,7 +95,9 @@ export async function fetchGpuBoard(client: Pick<GpuApi, "getCloudGpuBoard"> = a
|
|
|
94
95
|
try { payload = await client.getCloudGpuBoard(); }
|
|
95
96
|
catch (error) { throw unavailableOnServer(error, GPU_NOT_AVAILABLE, [404, 503]); }
|
|
96
97
|
const board = validateBoard(payload);
|
|
97
|
-
|
|
98
|
+
// The server's source did not answer this time: a saved copy answers, as it does a 5xx.
|
|
99
|
+
if (board.status === "unavailable") throw sourceDidNotAnswer(GPU_NOT_AVAILABLE);
|
|
100
|
+
if (!board.rows.length) throw new Error(GPU_NOT_AVAILABLE);
|
|
98
101
|
return board;
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadGpuBoard, loadGpuHistory } from "./client";
|
|
4
4
|
import { GPU_MODELS, gpuListIndex } from "./model";
|
|
5
5
|
|
|
@@ -27,12 +27,11 @@ export const GPU_SERIES: readonly SeriesDefinition[] = GPU_MODELS.map((model): S
|
|
|
27
27
|
if (!index) throw new Error(`No ${model} list index is listed yet.`);
|
|
28
28
|
const history = await loadGpuHistory(index.id);
|
|
29
29
|
if (history.payload.access?.locked) throw new Error(GPU_PRO_ONLY);
|
|
30
|
-
const warning = cachedSeriesWarning(history);
|
|
31
30
|
return {
|
|
32
31
|
points: history.payload.points
|
|
33
32
|
.filter((point) => `${point.source}:${point.skuKey}` === index.id)
|
|
34
33
|
.map((point) => ({ date: point.observedAt, value: point.pricePerGpuHr })),
|
|
35
|
-
...(
|
|
34
|
+
...cachedSeriesNote(history),
|
|
36
35
|
};
|
|
37
36
|
},
|
|
38
37
|
}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { formatActionChords, hasKeybindingCaptureRequest, subscribeKeybindingCapture, useKeybindings } from "../../../app/keybindings";
|
|
3
|
-
import { Notice,
|
|
3
|
+
import { Notice, usePaneFooter, usePaneMenuItems, usePaneTabs, type PaneHint, type TableSection } from "../../../components";
|
|
4
4
|
import { t, tf } from "../../../i18n";
|
|
5
5
|
import { colors } from "../../../theme/colors";
|
|
6
6
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -18,6 +18,7 @@ import { FunctionsTable } from "./functions-table";
|
|
|
18
18
|
import { KeybindingsEditor } from "./keybindings-editor";
|
|
19
19
|
import { ShortcutTable, type ShortcutTableEntry } from "./shortcut-table";
|
|
20
20
|
import { resolveCommandShortcuts, resolveWindowTemplates } from "./shortcut-model";
|
|
21
|
+
import { badgeColumnWidth } from "./table-cells";
|
|
21
22
|
|
|
22
23
|
const HELP_TABS = [
|
|
23
24
|
{ label: "Basics", value: "basics" },
|
|
@@ -47,6 +48,9 @@ const TRY_THESE = [
|
|
|
47
48
|
{ query: "EE TSLA", description: "What analysts expect from Tesla" },
|
|
48
49
|
] as const;
|
|
49
50
|
|
|
51
|
+
/** The most of a table its key column takes, however many chips a row carries. */
|
|
52
|
+
const KEY_COLUMN_MAX_SHARE = 0.4;
|
|
53
|
+
|
|
50
54
|
function HelpPane({ focused, width, height }: PaneProps) {
|
|
51
55
|
const registry = getSharedRegistry();
|
|
52
56
|
const { openCommandBar, showPane, createPaneFromTemplate } = usePluginAppActions();
|
|
@@ -83,7 +87,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
83
87
|
// The UPGRADE line says how long the trial is; the length comes from /pricing.
|
|
84
88
|
const [pricing, setPricing] = useState<CloudPricing | null>(null);
|
|
85
89
|
useEffect(() => {
|
|
86
|
-
if (activeTabId !== "
|
|
90
|
+
if (activeTabId !== "reference" || pricing) return;
|
|
87
91
|
let live = true;
|
|
88
92
|
void apiClient.getCloudPricing()
|
|
89
93
|
.then((next) => {
|
|
@@ -131,10 +135,11 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
131
135
|
// Each tab's actions. `l` moves between tabs, so layout actions take `a`.
|
|
132
136
|
const tabHints = useMemo<PaneHint[]>(() => {
|
|
133
137
|
if (activeTabId === "basics") {
|
|
134
|
-
return [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
return [{ id: "docs", key: "d", label: "ocs", title: "Open Docs", onPress: openDocs }];
|
|
139
|
+
}
|
|
140
|
+
// The layout rows are here, so their action is.
|
|
141
|
+
if (activeTabId === "reference") {
|
|
142
|
+
return [{ id: "layout-actions", key: "a", label: " layout actions", title: "Layout Actions", onPress: openLayoutActions }];
|
|
138
143
|
}
|
|
139
144
|
if (activeTabId === "functions") {
|
|
140
145
|
return [{ id: "plugins", key: "p", label: "lugins", title: "Manage Plugins", onPress: openPluginManager }];
|
|
@@ -192,32 +197,31 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
192
197
|
].filter((item) => hasCode(item.badges[0]!)),
|
|
193
198
|
}], [hasCode]);
|
|
194
199
|
|
|
195
|
-
const commandBarSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [{
|
|
196
|
-
label: "Command Bar",
|
|
197
|
-
items: [
|
|
198
|
-
...(commandBarBadges.length > 0
|
|
199
|
-
? [entry("open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
|
|
200
|
-
: []),
|
|
201
|
-
...(tickerSearchBadges.length > 0
|
|
202
|
-
? [entry("ticker-search", tickerSearchBadges, "Open ticker search directly.")]
|
|
203
|
-
: []),
|
|
204
|
-
entry("des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
|
|
205
|
-
entry("help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
|
|
206
|
-
entry("docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
|
|
207
|
-
entry("upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
|
|
208
|
-
entry("mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
|
|
209
|
-
entry("move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
|
|
210
|
-
entry("page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
|
|
211
|
-
entry("run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
|
|
212
|
-
entry("accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
|
|
213
|
-
entry("close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
|
|
214
|
-
entry("clear", ["Ctrl+U"], "Clear command text."),
|
|
215
|
-
entry("delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
|
|
216
|
-
entry("back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
|
|
217
|
-
],
|
|
218
|
-
}], [commandBarBadges, tickerSearchBadges, trialDays]);
|
|
219
|
-
|
|
220
200
|
const referenceSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [
|
|
201
|
+
{
|
|
202
|
+
label: "Command Bar",
|
|
203
|
+
items: [
|
|
204
|
+
...(commandBarBadges.length > 0
|
|
205
|
+
? [entry("cmd-open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
|
|
206
|
+
: []),
|
|
207
|
+
...(tickerSearchBadges.length > 0
|
|
208
|
+
? [entry("cmd-ticker-search", tickerSearchBadges, "Open ticker search directly.")]
|
|
209
|
+
: []),
|
|
210
|
+
entry("cmd-des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
|
|
211
|
+
entry("cmd-help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
|
|
212
|
+
entry("cmd-docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
|
|
213
|
+
entry("cmd-upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
|
|
214
|
+
entry("cmd-mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
|
|
215
|
+
entry("cmd-move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
|
|
216
|
+
entry("cmd-page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
|
|
217
|
+
entry("cmd-run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
|
|
218
|
+
entry("cmd-accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
|
|
219
|
+
entry("cmd-close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
|
|
220
|
+
entry("cmd-clear", ["Ctrl+U"], "Clear command text."),
|
|
221
|
+
entry("cmd-delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
|
|
222
|
+
entry("cmd-back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
|
|
223
|
+
],
|
|
224
|
+
},
|
|
221
225
|
{
|
|
222
226
|
label: "Navigation",
|
|
223
227
|
items: [
|
|
@@ -235,7 +239,8 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
235
239
|
entry("search", ["/"], "Search in the focused pane."),
|
|
236
240
|
entry("sidebar", ["<", ">"], "Narrow or widen the list beside the content, as in MAP and ASKG."),
|
|
237
241
|
entry("warnings", ["!"], "Open the focused pane's data warnings."),
|
|
238
|
-
entry("notification",
|
|
242
|
+
entry("notification", actionBadges("notification-action"), "Run the newest notification."),
|
|
243
|
+
entry("notification-dismiss", actionBadges("notification-dismiss"), "Dismiss the newest notification."),
|
|
239
244
|
],
|
|
240
245
|
},
|
|
241
246
|
{
|
|
@@ -303,6 +308,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
303
308
|
entry("close-pane", ["Esc", "Esc"], "Close the focused pane when nothing is being dragged."),
|
|
304
309
|
],
|
|
305
310
|
},
|
|
311
|
+
{
|
|
312
|
+
// Docked panes keep the saved layout and floating ones follow the pointer; the keyboard's way is Window Mode below.
|
|
313
|
+
label: "Layout",
|
|
314
|
+
items: [
|
|
315
|
+
entry("layout-drag", ["Drag"], "Floating panes can be dragged by the title bar and resized from the lower-right corner."),
|
|
316
|
+
{ ...entry("layout-actions", ["LMA"], "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets."), run: openLayoutActions },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
306
319
|
{
|
|
307
320
|
label: "Window Mode",
|
|
308
321
|
items: [
|
|
@@ -320,7 +333,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
320
333
|
entry("commit", ["Enter", "Esc"], "Commit pending changes or exit window mode."),
|
|
321
334
|
],
|
|
322
335
|
},
|
|
323
|
-
], [askBadges, copyBadges, functionHelpBadges, pasteBadges, windowMoveBadges, windowResizeBadges]);
|
|
336
|
+
], [askBadges, commandBarBadges, copyBadges, functionHelpBadges, openLayoutActions, pasteBadges, tickerSearchBadges, trialDays, windowMoveBadges, windowResizeBadges]);
|
|
337
|
+
|
|
338
|
+
// One key column for every key table in Help, wide enough for the longest group of chips, so the descriptions
|
|
339
|
+
// line up from table to table. A narrow pane caps it, and a row past the cap shows the chips that fit whole.
|
|
340
|
+
const keyColumnWidth = useMemo(() => badgeColumnWidth(
|
|
341
|
+
[...trySections, ...startSections, ...referenceSections].flatMap((section) => section.items.map((item) => item.badges)),
|
|
342
|
+
{ min: 8, max: Math.max(8, Math.floor(width * KEY_COLUMN_MAX_SHARE)) },
|
|
343
|
+
), [referenceSections, startSections, trySections, width]);
|
|
324
344
|
|
|
325
345
|
const renderContent = () => {
|
|
326
346
|
switch (activeTabId) {
|
|
@@ -339,38 +359,19 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
339
359
|
<>
|
|
340
360
|
{trySections.length > 0 && (
|
|
341
361
|
<Box marginBottom={1}>
|
|
342
|
-
<ShortcutTable sections={trySections} width={bodyWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
|
|
362
|
+
<ShortcutTable sections={trySections} width={bodyWidth} keyWidth={keyColumnWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
|
|
343
363
|
</Box>
|
|
344
364
|
)}
|
|
345
365
|
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{commandBarBadges[0]
|
|
352
|
-
? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
|
|
353
|
-
: t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
|
|
354
|
-
</Text>
|
|
355
|
-
</Box>
|
|
356
|
-
</Box>
|
|
366
|
+
<Text fg={colors.textDim} wrapText>
|
|
367
|
+
{commandBarBadges[0]
|
|
368
|
+
? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
|
|
369
|
+
: t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
|
|
370
|
+
</Text>
|
|
357
371
|
|
|
358
372
|
<Box marginTop={1}>
|
|
359
|
-
<ShortcutTable sections={startSections} width={bodyWidth} />
|
|
373
|
+
<ShortcutTable sections={startSections} width={bodyWidth} keyWidth={keyColumnWidth} />
|
|
360
374
|
</Box>
|
|
361
|
-
|
|
362
|
-
<Box marginTop={1}>
|
|
363
|
-
<ShortcutTable sections={commandBarSections} width={bodyWidth} />
|
|
364
|
-
</Box>
|
|
365
|
-
|
|
366
|
-
<Section title="Layout Basics">
|
|
367
|
-
<Text fg={colors.text}>{t("Docked panes stay in the saved layout.")}</Text>
|
|
368
|
-
<Text fg={colors.text} wrapText>{t("Floating panes can be dragged by the title bar and resized from the lower-right corner.")}</Text>
|
|
369
|
-
{windowMoveBadges[0] && windowResizeBadges[0] && (
|
|
370
|
-
<Text fg={colors.text} wrapText>{tf("From the keyboard, {move} moves the focused window and {resize} resizes it.", { move: windowMoveBadges[0], resize: windowResizeBadges[0] })}</Text>
|
|
371
|
-
)}
|
|
372
|
-
<Text fg={colors.text} wrapText>{t("Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.")}</Text>
|
|
373
|
-
</Section>
|
|
374
375
|
</>
|
|
375
376
|
);
|
|
376
377
|
}
|
|
@@ -396,6 +397,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
396
397
|
<ShortcutTable
|
|
397
398
|
sections={referenceSections}
|
|
398
399
|
width={width}
|
|
400
|
+
keyWidth={keyColumnWidth}
|
|
399
401
|
height={contentHeight}
|
|
400
402
|
after={(
|
|
401
403
|
<Box paddingX={1} flexShrink={0}>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { t } from "../../../i18n";
|
|
23
23
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
24
24
|
import { Box, useUiCapabilities } from "../../../ui";
|
|
25
|
-
import { badgeCell
|
|
25
|
+
import { badgeCell } from "./table-cells";
|
|
26
26
|
|
|
27
27
|
export interface ShortcutTableEntry {
|
|
28
28
|
id: string;
|
|
@@ -38,6 +38,7 @@ type ShortcutRow = SectionedRow<ShortcutTableEntry>;
|
|
|
38
38
|
export function ShortcutTable({
|
|
39
39
|
sections,
|
|
40
40
|
width,
|
|
41
|
+
keyWidth,
|
|
41
42
|
height: fillHeight,
|
|
42
43
|
after,
|
|
43
44
|
keyLabel = "KEY",
|
|
@@ -46,6 +47,8 @@ export function ShortcutTable({
|
|
|
46
47
|
sections: ReadonlyArray<TableSection<ShortcutTableEntry>>;
|
|
47
48
|
/** Content width available inside the tab body. */
|
|
48
49
|
width: number;
|
|
50
|
+
/** The key column's width, one for every table in Help so their descriptions line up. */
|
|
51
|
+
keyWidth: number;
|
|
49
52
|
/** Fill this many rows and scroll, instead of growing to fit every row. */
|
|
50
53
|
height?: number;
|
|
51
54
|
/** Sits under a full-height table inside the same frame. */
|
|
@@ -59,10 +62,6 @@ export function ShortcutTable({
|
|
|
59
62
|
() => buildSectionedRows(sections, (entry) => entry.id),
|
|
60
63
|
[sections],
|
|
61
64
|
);
|
|
62
|
-
const keyWidth = useMemo(
|
|
63
|
-
() => badgeColumnWidth(rows.filter(isSectionedItemRow).map((row) => row.item.badges), { min: 8, max: 24 }),
|
|
64
|
-
[rows],
|
|
65
|
-
);
|
|
66
65
|
const columns = useMemo<ShortcutColumn[]>(() => [
|
|
67
66
|
{ id: "key", label: keyLabel, width: keyWidth, align: "left" },
|
|
68
67
|
{ id: "description", label: descriptionLabel, width: 20, align: "left", flexGrow: 1 },
|
|
@@ -72,7 +71,7 @@ export function ShortcutTable({
|
|
|
72
71
|
if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
|
|
73
72
|
const run = row.item.run;
|
|
74
73
|
const cell: DataTableCell = column.id === "key"
|
|
75
|
-
? badgeCell(row.item.badges, column.width)
|
|
74
|
+
? badgeCell(row.item.badges, column.width, { wholeOnly: true })
|
|
76
75
|
: { text: t(row.item.description), color: colors.text };
|
|
77
76
|
if (!run) return cell;
|
|
78
77
|
return {
|