gloomberb 0.18.4 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/data.ts +14 -0
- package/src/api-client/index.ts +160 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/members.ts +12 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/types.ts +6 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +68 -22
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/overview.ts +119 -31
- package/src/cli/commands/search.ts +18 -2
- package/src/cli/commands/ticker.ts +2 -1
- package/src/cli/errors.ts +2 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +49 -0
- package/src/cli/history-rows.ts +30 -7
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +16 -5
- package/src/cli/pane-functions/freshness.ts +25 -13
- package/src/cli/pane-functions/function-help.ts +10 -3
- package/src/cli/pane-functions/headless.ts +39 -5
- package/src/cli/pane-functions/index.ts +10 -3
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +18 -2
- package/src/cli/quote-answer.ts +10 -4
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/components/chart/composite/composite-chart.tsx +22 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +9 -2
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/scene.ts +7 -3
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/ui/display.tsx +5 -1
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/saved-copies.ts +55 -16
- package/src/docs/snapshot.json +361 -84
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/market/change-basis.ts +33 -5
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli.ts +59 -20
- package/src/plugins/builtin/alerts/command.ts +7 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/fired-log.ts +45 -4
- package/src/plugins/builtin/alerts/format.ts +8 -2
- package/src/plugins/builtin/alerts/headless.ts +10 -5
- package/src/plugins/builtin/alerts/history-rows.ts +5 -2
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +66 -16
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +42 -29
- package/src/plugins/builtin/alerts/storage.ts +33 -10
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
- package/src/plugins/builtin/chart-composer/headless.ts +13 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
- package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/client.ts +6 -1
- package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
- package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/iv-history/headless.ts +3 -0
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +25 -8
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/pane.tsx +15 -11
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -3
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +6 -4
- package/src/plugins/builtin/vol-surface/headless.ts +62 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/portfolio/column-values.ts +5 -5
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +66 -16
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/theme/colors.ts +8 -0
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +155 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +5 -1
- package/src/time-series/resolve.ts +61 -12
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/spec.ts +2 -1
- package/src/time-series/studies.ts +270 -72
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +5 -0
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +41 -1
- package/src/types/headless.ts +42 -4
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +1 -1
- package/src/updater/command-names.ts +12 -7
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +5 -0
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -330,11 +330,15 @@ function buildAxisDomain(
|
|
|
330
330
|
scale: PanelScale,
|
|
331
331
|
rows: number,
|
|
332
332
|
manualScale?: CompositeManualValueScale,
|
|
333
|
+
referenceLevels?: BuildCompositeChartSceneOptions["referenceLevels"],
|
|
333
334
|
): CompositeAxisDomain | undefined {
|
|
334
335
|
const axisSeries = series.filter((entry) => entry.axis === side);
|
|
335
336
|
if (axisSeries.length === 0) return undefined;
|
|
336
337
|
const values = axisSeries.flatMap(seriesDomainValues);
|
|
337
|
-
const
|
|
338
|
+
const references = referenceLevels && axisSeries.some((entry) => entry.id === referenceLevels.seriesId)
|
|
339
|
+
? referenceLevels.items.map((level) => level.value).filter(Number.isFinite)
|
|
340
|
+
: [];
|
|
341
|
+
const fitted = paddedDomain([...values, ...priorCloseInReach(axisSeries, values), ...references], scale);
|
|
338
342
|
const scaleKey = valueScaleKey(scale, axisSeries);
|
|
339
343
|
// A range set by hand holds while the axis plots the same series and some
|
|
340
344
|
// of their data is in it; otherwise the axis fits the data again.
|
|
@@ -849,8 +853,8 @@ export function buildCompositeChartScene(
|
|
|
849
853
|
? dataSeries.filter((entry) => entry.panelId === panel.id)
|
|
850
854
|
: panelSeries;
|
|
851
855
|
const rows = panelHeights.get(panel.id) ?? 1;
|
|
852
|
-
const left = buildAxisDomain("left", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "left")]);
|
|
853
|
-
const right = buildAxisDomain("right", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "right")]);
|
|
856
|
+
const left = buildAxisDomain("left", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "left")], options.referenceLevels);
|
|
857
|
+
const right = buildAxisDomain("right", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "right")], options.referenceLevels);
|
|
854
858
|
const axes: Partial<Record<CompositeAxisSide, CompositeAxisDomain>> = { left, right };
|
|
855
859
|
return {
|
|
856
860
|
id: panel.id,
|
|
@@ -7,20 +7,23 @@ import type { SeriesAxisTag } from "./panel-labels";
|
|
|
7
7
|
* ties. A forward pass separates neighbours; a backward pass fits the bottom
|
|
8
8
|
* edge. Panels shorter than the readout use parallel lanes, not dropped tags
|
|
9
9
|
* or smaller type. The gutter reserves those lanes before the cursor moves.
|
|
10
|
+
* Only a tag pushed off its value's height gets a leader: a tick beside a tag
|
|
11
|
+
* still on its value reads as a minus sign (`-8k` for a positive 8k).
|
|
10
12
|
*/
|
|
11
13
|
export function placeSeriesAxisTags(tags: readonly SeriesAxisTag[], pixelHeight: number, labelHeight: number) {
|
|
12
14
|
const height = Math.max(pixelHeight, labelHeight);
|
|
13
15
|
const capacity = Math.max(1, Math.floor(height / labelHeight));
|
|
14
16
|
const sorted = tags.map((tag, order) => ({ ...tag, order, anchorY: clamp(tag.yRatio, 0, 1) * Math.max(pixelHeight - 1, 0) }))
|
|
15
17
|
.sort((a, b) => a.anchorY - b.anchorY || a.order - b.order);
|
|
16
|
-
const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number }> = [];
|
|
18
|
+
const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number; leader: boolean }> = [];
|
|
17
19
|
for (let start = 0; start < sorted.length; start += capacity) {
|
|
18
20
|
const lane = sorted.slice(start, start + capacity);
|
|
19
21
|
const centers = lane.map((tag) => clamp(tag.anchorY, labelHeight / 2, height - labelHeight / 2));
|
|
20
22
|
for (let i = 1; i < centers.length; i++) centers[i] = Math.max(centers[i]!, centers[i - 1]! + labelHeight);
|
|
21
23
|
if (centers.length) centers[centers.length - 1] = Math.min(centers.at(-1)!, height - labelHeight / 2);
|
|
22
24
|
for (let i = centers.length - 2; i >= 0; i--) centers[i] = Math.min(centers[i]!, centers[i + 1]! - labelHeight);
|
|
23
|
-
lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity)
|
|
25
|
+
lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity),
|
|
26
|
+
leader: Math.abs(centers[index]! - tag.anchorY) > labelHeight / 2 }));
|
|
24
27
|
}
|
|
25
28
|
return placed;
|
|
26
29
|
}
|
|
@@ -37,13 +40,14 @@ export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
|
|
|
37
40
|
const edge = left ? "right" : "left";
|
|
38
41
|
const inset = `${tag.lane * 100 / lanes}%`;
|
|
39
42
|
return <Box key={tag.seriesId} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
|
|
40
|
-
{/* The
|
|
43
|
+
{/* The anchor stays at the observation; it draws a tick when the badge had to move off it. */}
|
|
41
44
|
<Box
|
|
42
45
|
data-gloom-role="chart-series-axis-anchor"
|
|
43
46
|
data-series-id={tag.seriesId}
|
|
44
|
-
|
|
47
|
+
data-leader={tag.leader ? "true" : undefined}
|
|
48
|
+
style={{ position: "absolute", [edge]: 0, top: percent(tag.anchorY), width: `calc(${inset} + .5em)`, height: 0, borderTop: tag.leader ? `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}` : undefined }}
|
|
45
49
|
/>
|
|
46
|
-
<Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} />
|
|
50
|
+
{tag.leader ? <Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} /> : null}
|
|
47
51
|
<Box
|
|
48
52
|
data-gloom-role="chart-series-axis-tag"
|
|
49
53
|
data-series-id={tag.seriesId}
|
|
@@ -52,12 +56,15 @@ export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
|
|
|
52
56
|
data-carried={tag.carried ? "true" : undefined}
|
|
53
57
|
aria-label={`${tag.seriesLabel}: ${tag.label}${tag.carried ? " (carried)" : ""}`}
|
|
54
58
|
style={{
|
|
55
|
-
position: "absolute", [edge]:
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
position: "absolute", [edge]: inset, top: percent(tag.centerY), transform: "translateY(-50%)",
|
|
60
|
+
// The value sits in a box in its series colour, as a chart level's label does, so nothing
|
|
61
|
+
// beside it can read as the value's sign. A box wider than its lane is clipped, not wrapped.
|
|
62
|
+
width: "max-content", maxWidth: `calc(${100 / lanes}% - ${lanes > 1 ? ".25em" : "0px"})`,
|
|
63
|
+
boxSizing: "border-box", height: "1.5em", lineHeight: "1.5em", paddingInline: ".15em",
|
|
64
|
+
alignItems: left ? "flex-end" : "flex-start", justifyContent: "center", overflow: "hidden",
|
|
58
65
|
// Every badge paints above every leader, including another lane's.
|
|
59
66
|
zIndex: 2, color: tag.color, backgroundColor: background, whiteSpace: "nowrap",
|
|
60
|
-
|
|
67
|
+
border: `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}`,
|
|
61
68
|
}}
|
|
62
69
|
>
|
|
63
70
|
<Text fg={tag.color} selectable={false}>{tag.label}</Text>
|
|
@@ -210,6 +210,8 @@ export interface BuildCompositeChartSceneOptions {
|
|
|
210
210
|
rightOffsetRatio?: number;
|
|
211
211
|
/** Value axes stretched or squeezed by hand, in place of fitting them to the data. */
|
|
212
212
|
valueScales?: CompositeValueScales;
|
|
213
|
+
/** Reference guides participate in auto-scaling on the axis of this series. */
|
|
214
|
+
referenceLevels?: CompositeChartLevels | null;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
export interface CompositeChartXMarker {
|
|
@@ -347,4 +349,6 @@ export interface CompositeChartProps {
|
|
|
347
349
|
* tool (Shift+H) adds, moves and deletes them by mouse and keyboard.
|
|
348
350
|
*/
|
|
349
351
|
levels?: CompositeChartLevels | null;
|
|
352
|
+
/** Mean and population ±1σ of at least 20 finite observations in the active viewport. */
|
|
353
|
+
rangeStatisticsSeriesId?: string;
|
|
350
354
|
}
|
|
@@ -52,11 +52,3 @@ function getSnapshot(): CommandBarPromptBinding | null {
|
|
|
52
52
|
export function useCommandBarPromptBinding(): CommandBarPromptBinding | null {
|
|
53
53
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Calls `listener` with the open screen's query each time the bar publishes
|
|
58
|
-
* its prompt, and with an empty one when it closes. For code outside React.
|
|
59
|
-
*/
|
|
60
|
-
export function subscribeCommandBarQuery(listener: (query: string) => void): () => void {
|
|
61
|
-
return subscribe(() => listener(binding?.query ?? ""));
|
|
62
|
-
}
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
const SET_ALERT_WORKFLOW_ID = "plugin-command:set-alert";
|
|
14
14
|
|
|
15
15
|
function isSetAlertWorkflow(route: CommandBarWorkflowRoute): boolean {
|
|
16
|
-
return route.workflowId === SET_ALERT_WORKFLOW_ID;
|
|
16
|
+
return route.workflowId === SET_ALERT_WORKFLOW_ID && (!route.values.kind || route.values.kind === "price");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function normalizeAlertWorkflowSymbol(value: CommandBarFieldValue | undefined): string {
|
|
@@ -38,17 +38,9 @@ import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-control
|
|
|
38
38
|
import { Button, terminalButtonColumns } from "../ui/button";
|
|
39
39
|
|
|
40
40
|
const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
41
|
-
/**
|
|
42
|
-
* The failed-update message is cut to the room it has, but not below this many
|
|
43
|
-
* columns ("Update fa..."): narrower says nothing, so the banner is left out.
|
|
44
|
-
* The retry hint gives way before the message goes under it.
|
|
45
|
-
*/
|
|
46
|
-
const UPDATE_ERROR_MIN_COLUMNS = 12;
|
|
47
41
|
/** Kept clear after the terminal update status, so its last character never touches the Help pill. */
|
|
48
42
|
const UPDATE_STATUS_GAP_COLUMNS = 1;
|
|
49
|
-
/**
|
|
50
|
-
const UPDATE_REASON_MIN_COLUMNS = 8;
|
|
51
|
-
/** A terminal line about an update narrower than this is left out rather than shown as a stub. */
|
|
43
|
+
/** A clipped terminal notice narrower than this is left out rather than shown as a stub. */
|
|
52
44
|
const UPDATE_TEXT_MIN_COLUMNS = 8;
|
|
53
45
|
/** Left padding of the update status's slot, in cells; the desktop's title-bar layout also pads its right. */
|
|
54
46
|
const UPDATE_SLOT_PADDING_COLUMNS = 2;
|
|
@@ -316,7 +308,7 @@ function HeaderCommandPrompt({
|
|
|
316
308
|
*/
|
|
317
309
|
type UpdateStatusView =
|
|
318
310
|
| { kind: "busy"; text: string; parts: StatusPart[] }
|
|
319
|
-
| { kind: "text"; text: string }
|
|
311
|
+
| { kind: "text"; text: string; compact?: string; clip?: boolean }
|
|
320
312
|
| { kind: "restart"; text: string; version: string }
|
|
321
313
|
| { kind: "failed"; message: string; parts: StatusPart[]; retry: boolean };
|
|
322
314
|
|
|
@@ -374,7 +366,7 @@ function resolveUpdateStatusView({
|
|
|
374
366
|
if (updateProgress.canRestart && canRestart) {
|
|
375
367
|
return { kind: "restart", text: t("Update ready"), version: updateAvailable?.version ?? "" };
|
|
376
368
|
}
|
|
377
|
-
return { kind: "text", text: t("Update ready, restart to apply") };
|
|
369
|
+
return { kind: "text", text: t("Update ready, restart to apply"), compact: t("Update ready") };
|
|
378
370
|
}
|
|
379
371
|
if (updateProgress.phase === "done") {
|
|
380
372
|
return { kind: "text", text: t(updateProgress.message ?? "Update ready, restart to apply") };
|
|
@@ -419,7 +411,7 @@ function resolveUpdateStatusView({
|
|
|
419
411
|
}
|
|
420
412
|
return { kind: "text", text: tf("v{version} available — starting download...", { version: updateAvailable.version }) };
|
|
421
413
|
}
|
|
422
|
-
if (updateNotice) return { kind: "text", text: updateNotice };
|
|
414
|
+
if (updateNotice) return { kind: "text", text: updateNotice, clip: true };
|
|
423
415
|
return null;
|
|
424
416
|
}
|
|
425
417
|
|
|
@@ -460,8 +452,8 @@ function retryHint(view: Extract<UpdateStatusView, { kind: "failed" }>, updateKe
|
|
|
460
452
|
}
|
|
461
453
|
|
|
462
454
|
/**
|
|
463
|
-
* Columns the terminal status takes when
|
|
464
|
-
* slot that much from the closed prompt's empty room before
|
|
455
|
+
* Columns the terminal status takes when everything fits: the header gives
|
|
456
|
+
* the slot that much from the closed prompt's empty room before dropping parts.
|
|
465
457
|
*/
|
|
466
458
|
function terminalUpdateStatusColumns(view: UpdateStatusView, updateKey: string): number {
|
|
467
459
|
return UPDATE_STATUS_GAP_COLUMNS + terminalUpdateStatusContentColumns(view, updateKey);
|
|
@@ -490,7 +482,7 @@ function terminalClosedPromptFloor(): number {
|
|
|
490
482
|
}
|
|
491
483
|
|
|
492
484
|
/**
|
|
493
|
-
* What the header says about an update. The terminal
|
|
485
|
+
* What the header says about an update. The terminal fits it to `columns`,
|
|
494
486
|
* the room its slot is guaranteed; the desktop and the web let the browser fit
|
|
495
487
|
* it to the room the bar leaves (NativeUpdateStatus). Either way it never runs
|
|
496
488
|
* into the Help pill or the market cluster beside it.
|
|
@@ -525,7 +517,7 @@ function TerminalUpdateStatus({ columns, onRestart, updateKey, view }: {
|
|
|
525
517
|
);
|
|
526
518
|
}
|
|
527
519
|
|
|
528
|
-
/**
|
|
520
|
+
/** Whole status parts in priority order; only transient notices may be clipped. */
|
|
529
521
|
function terminalUpdateStatusContent({ onRestart, room, updateKey, view }: {
|
|
530
522
|
onRestart?: () => void;
|
|
531
523
|
room: number;
|
|
@@ -533,9 +525,9 @@ function terminalUpdateStatusContent({ onRestart, room, updateKey, view }: {
|
|
|
533
525
|
view: UpdateStatusView;
|
|
534
526
|
}): ReactNode {
|
|
535
527
|
if (view.kind === "busy") {
|
|
536
|
-
// The
|
|
537
|
-
|
|
538
|
-
return <TerminalBusyStatus text={
|
|
528
|
+
// The percent stays when the words give way; the spinner never stands alone.
|
|
529
|
+
const text = fitStatusParts(view.parts, room - 2);
|
|
530
|
+
return text ? <TerminalBusyStatus text={text} /> : null;
|
|
539
531
|
}
|
|
540
532
|
if (view.kind === "restart") {
|
|
541
533
|
// The button stays when the words do not fit beside it.
|
|
@@ -549,9 +541,7 @@ function terminalUpdateStatusContent({ onRestart, room, updateKey, view }: {
|
|
|
549
541
|
const hintColumns = displayWidth(hint);
|
|
550
542
|
const messageColumns = displayWidth(view.message);
|
|
551
543
|
const short = t(UPDATE_FAILED_SHORT);
|
|
552
|
-
|
|
553
|
-
// The whole message with its key, then the message alone, then as much of
|
|
554
|
-
// the reason as reads, then "Update failed" with its key; never less.
|
|
544
|
+
// The reason and retry hint give way whole, leaving a compact failure signal.
|
|
555
545
|
let text: string;
|
|
556
546
|
let showHint = false;
|
|
557
547
|
if (messageColumns + hintColumns <= room) {
|
|
@@ -559,37 +549,33 @@ function terminalUpdateStatusContent({ onRestart, room, updateKey, view }: {
|
|
|
559
549
|
showHint = hintColumns > 0;
|
|
560
550
|
} else if (messageColumns <= room) {
|
|
561
551
|
text = view.message;
|
|
562
|
-
} else if (reasonRoom >= UPDATE_REASON_MIN_COLUMNS) {
|
|
563
|
-
text = truncateToDisplayWidth(view.message, room);
|
|
564
552
|
} else if (displayWidth(short) + hintColumns <= room) {
|
|
565
553
|
text = short;
|
|
566
554
|
showHint = hintColumns > 0;
|
|
567
|
-
} else if (
|
|
568
|
-
text =
|
|
555
|
+
} else if (displayWidth(short) <= room) {
|
|
556
|
+
text = short;
|
|
569
557
|
} else {
|
|
570
558
|
return null;
|
|
571
559
|
}
|
|
572
560
|
return <TerminalFailedStatus text={text} hint={showHint ? hint : ""} />;
|
|
573
561
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
562
|
+
if (view.clip) {
|
|
563
|
+
if (room < UPDATE_TEXT_MIN_COLUMNS) return null;
|
|
564
|
+
return <TerminalTextStatus text={view.text} width={room} />;
|
|
565
|
+
}
|
|
566
|
+
const text = displayWidth(view.text) <= room ? view.text : view.compact;
|
|
567
|
+
return text && displayWidth(text) <= room ? <TerminalTextStatus text={text} width={room} /> : null;
|
|
577
568
|
}
|
|
578
569
|
|
|
579
570
|
/**
|
|
580
|
-
* A status line in `width` columns: the
|
|
581
|
-
*
|
|
571
|
+
* A status line in `width` columns: the parts the desktop may clip instead
|
|
572
|
+
* drop out whole in the terminal, keeping the download's percent readable.
|
|
582
573
|
*/
|
|
583
574
|
function fitStatusParts(parts: StatusPart[], width: number): string {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
// A cut piece keeps a space before the whole one after it: "Downloading v... 42%".
|
|
589
|
-
const cut = (part: StatusPart, index: number) => index < parts.length - 1
|
|
590
|
-
? `${truncateToDisplayWidth(part.text.trimEnd(), Math.max(0, cutRoom - 1))} `
|
|
591
|
-
: truncateToDisplayWidth(part.text, cutRoom);
|
|
592
|
-
return truncateToDisplayWidth(parts.map((part, index) => (part.cut ? cut(part, index) : part.text)).join(""), width);
|
|
575
|
+
const full = parts.map((part) => part.text).join("");
|
|
576
|
+
if (displayWidth(full) <= width) return full;
|
|
577
|
+
const compact = parts.filter((part) => !part.cut).map((part) => part.text).join("").trim();
|
|
578
|
+
return displayWidth(compact) <= width ? compact : "";
|
|
593
579
|
}
|
|
594
580
|
|
|
595
581
|
function TerminalBusyStatus({ text }: { text: string }) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContextMenuItem } from "../../../../types/context-menu";
|
|
2
2
|
import { t } from "../../../../i18n";
|
|
3
3
|
import { clipToDisplayWidth, displayWidth } from "../../../../utils/format";
|
|
4
|
+
import { formatShortDate } from "../../../../utils/datetime-format";
|
|
4
5
|
import { getShortcutHintWidth } from "../../../ui/shortcut-hint";
|
|
5
6
|
|
|
6
7
|
export interface PaneFooterRegistration {
|
|
@@ -42,6 +43,12 @@ export interface PaneFooterSegment {
|
|
|
42
43
|
* line states its data's date and delay leaves it out.
|
|
43
44
|
*/
|
|
44
45
|
quoteStatus?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The segment says what the rows leave out (releases a filter hides), which
|
|
48
|
+
* a picture of the rows cannot: a `gloom shot` keeps it, as it keeps a
|
|
49
|
+
* warning.
|
|
50
|
+
*/
|
|
51
|
+
capture?: boolean;
|
|
45
52
|
/**
|
|
46
53
|
* Shorter forms of `parts`, longest first. When the row is too narrow for
|
|
47
54
|
* `parts` beside the hints, it shows the first that fits, or the last, and
|
|
@@ -90,6 +97,26 @@ export function delayedFooterSegment({
|
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The footer's one form for data shown from a copy saved earlier because
|
|
102
|
+
* refreshing it failed (offline, a hung or refused proxy): `saved copy from
|
|
103
|
+
* 14:37`, or with its day when saved on another. It says how current the
|
|
104
|
+
* pane's quotes are, so it is a quote status.
|
|
105
|
+
*/
|
|
106
|
+
export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.now()): PaneFooterSegment {
|
|
107
|
+
const saved = new Date(savedAt);
|
|
108
|
+
const time = saved.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
109
|
+
const when = saved.toDateString() === new Date(now).toDateString() ? time : `${formatShortDate(saved, { year: false })} ${time}`;
|
|
110
|
+
const text = `${t("saved copy from")} ${when}`;
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
quoteStatus: true,
|
|
114
|
+
title: `${t("Couldn't refresh")}: ${text}`,
|
|
115
|
+
parts: [{ text, tone: "warning" }],
|
|
116
|
+
variants: [[{ text: t("saved copy"), tone: "warning" }]],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
/** A pane whose market data is all delayed, by no one figure: the clock alone, its tooltip saying so. */
|
|
94
121
|
export const DELAYED_SEGMENT: PaneFooterSegment = delayedFooterSegment({ id: "delayed", detail: "Delayed market data" });
|
|
95
122
|
|
|
@@ -343,7 +343,7 @@ export function OnboardingHeader({
|
|
|
343
343
|
if (!desktop) {
|
|
344
344
|
return (
|
|
345
345
|
<Box height={1} flexDirection="row" justifyContent="space-between">
|
|
346
|
-
<Text fg={colors.textMuted}>{t("
|
|
346
|
+
<Text fg={colors.textMuted}>{t("GLOOM SETUP")}</Text>
|
|
347
347
|
{showDismiss ? (
|
|
348
348
|
<Button
|
|
349
349
|
label={dismissing ? "Closing..." : "Skip setup"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type ComponentProps, type RefObject } from "react";
|
|
2
2
|
import { Box, Text, TextAttributes, useUiCapabilities, type InputRenderable } from "../../../ui";
|
|
3
3
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
4
|
-
import {
|
|
4
|
+
import { shownPriceColor } from "../../../theme/colors";
|
|
5
5
|
import { t, tf } from "../../../i18n";
|
|
6
6
|
import { formatMarketPrice } from "../../../market-data/market/format";
|
|
7
7
|
import { getRegularSessionDisplay } from "../../../market-data/market/status";
|
|
@@ -62,7 +62,7 @@ function PreviewLine({ preview, error }: Pick<OnboardingPositionsState, "preview
|
|
|
62
62
|
<Text fg={colors.text} attributes={TextAttributes.BOLD}>{preview.symbol}</Text>
|
|
63
63
|
{preview.name ? <Text fg={colors.textDim}>{` ${preview.name}`}</Text> : null}
|
|
64
64
|
{price != null ? <Text fg={colors.text}>{` ${formatMarketPrice(price, { maxWidth: 12 })}`}</Text> : null}
|
|
65
|
-
{change != null ? <Text fg={
|
|
65
|
+
{change != null ? <Text fg={shownPriceColor(change, 2, colors)}>{` ${formatPercentRaw(change)}`}</Text> : null}
|
|
66
66
|
{preview.duplicate ? <Text fg={colors.textMuted}>{t(" already added")}</Text> : null}
|
|
67
67
|
</Box>
|
|
68
68
|
);
|
|
@@ -135,8 +135,12 @@ function NativeKeyValueRow({ label, value, detail, tone, color, width, labelWidt
|
|
|
135
135
|
} : {};
|
|
136
136
|
const rowWidth = width === undefined ? undefined : Math.max(0, Math.floor(width));
|
|
137
137
|
const labelChars = Math.max(0, labelWidth ?? (rowWidth === undefined ? 14 : Math.min(12, Math.max(8, Math.floor(rowWidth * 0.32)))));
|
|
138
|
+
// A bounded row fills its parent's width. Rows side by side (a pair of metrics) each give way to an equal
|
|
139
|
+
// share of their row, so the second is not pushed past the pane edge; never shorter than its line, so a
|
|
140
|
+
// column with less room than its rows still overflows and scrolls instead of squeezing them.
|
|
141
|
+
const bounded = width === undefined ? {} : { width: "100%", flexShrink: 1, minHeight: "min-content" };
|
|
138
142
|
return (
|
|
139
|
-
<Box
|
|
143
|
+
<Box {...bounded} flexDirection="row" overflow="hidden" data-gloom-ui="key-value-row" {...target}>
|
|
140
144
|
{/* At least the label width, in characters; a longer label keeps every character and moves its value along. */}
|
|
141
145
|
<Text fg={colors.textDim} style={{ ...NATIVE_CLIP_STYLE, flex: "0 0 auto", minWidth: `${labelChars}ch`, paddingRight: "1ch", boxSizing: "border-box" }}>{t(label)}</Text>
|
|
142
146
|
{country ? <CountryFlagLead value={country} /> : null}
|
|
@@ -594,6 +594,7 @@ export function withFocusedPane(
|
|
|
594
594
|
config: syncedConfig,
|
|
595
595
|
paneState: nextPaneState,
|
|
596
596
|
brokerAccounts: reconcileBrokerAccounts(syncedConfig, state.brokerAccounts),
|
|
597
|
+
brokerSyncErrors: Object.fromEntries(Object.entries(state.brokerSyncErrors).filter(([id]) => syncedConfig.brokerInstances.some((instance) => instance.id === id))),
|
|
597
598
|
focusedPaneId,
|
|
598
599
|
previousFocusedPaneId: state.focusedPaneId && state.focusedPaneId !== focusedPaneId
|
|
599
600
|
? state.focusedPaneId
|
|
@@ -605,6 +606,7 @@ export function hydrateDesktopSnapshot(state: AppState, snapshot: DesktopSharedS
|
|
|
605
606
|
return withFocusedPane({
|
|
606
607
|
...state,
|
|
607
608
|
paneState: snapshot.paneState,
|
|
609
|
+
brokerSyncErrors: snapshot.brokerSyncErrors ?? {},
|
|
608
610
|
focusedPaneId: snapshot.focusedPaneId,
|
|
609
611
|
statusBarVisible: snapshot.statusBarVisible,
|
|
610
612
|
}, snapshot.config);
|
|
@@ -203,6 +203,14 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
203
203
|
},
|
|
204
204
|
};
|
|
205
205
|
|
|
206
|
+
case "SET_BROKER_SYNC_ERROR": {
|
|
207
|
+
if (!state.config.brokerInstances.some((instance) => instance.id === action.instanceId)) return state;
|
|
208
|
+
const brokerSyncErrors = { ...state.brokerSyncErrors };
|
|
209
|
+
if (action.error) brokerSyncErrors[action.instanceId] = action.error;
|
|
210
|
+
else delete brokerSyncErrors[action.instanceId];
|
|
211
|
+
return { ...state, brokerSyncErrors };
|
|
212
|
+
}
|
|
213
|
+
|
|
206
214
|
case "SET_INITIALIZED":
|
|
207
215
|
return { ...state, initialized: true };
|
|
208
216
|
|
|
@@ -371,6 +379,7 @@ export function createInitialState(config: AppConfig, sessionSnapshot: AppSessio
|
|
|
371
379
|
financials: new Map(),
|
|
372
380
|
exchangeRates: new Map([["USD", 1]]),
|
|
373
381
|
brokerAccounts: {},
|
|
382
|
+
brokerSyncErrors: {},
|
|
374
383
|
focusedPaneId,
|
|
375
384
|
previousFocusedPaneId: null,
|
|
376
385
|
paneState,
|
|
@@ -68,6 +68,8 @@ export interface AppState {
|
|
|
68
68
|
*/
|
|
69
69
|
exchangeRates: Map<string, number>;
|
|
70
70
|
brokerAccounts: Record<string, BrokerAccount[]>;
|
|
71
|
+
/** A failed position import stays visible until this profile syncs successfully. */
|
|
72
|
+
brokerSyncErrors: Record<string, string>;
|
|
71
73
|
/**
|
|
72
74
|
* @deprecated Never set. The app has no left or right panel state; read
|
|
73
75
|
* `focusedPaneId` for the focused pane.
|
|
@@ -131,6 +133,7 @@ export type AppAction =
|
|
|
131
133
|
| { type: "RECORD_COMMAND"; id: string; label: string; arg?: string }
|
|
132
134
|
| { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
|
|
133
135
|
| { type: "SET_BROKER_ACCOUNTS"; instanceId: string; accounts: BrokerAccount[] }
|
|
136
|
+
| { type: "SET_BROKER_SYNC_ERROR"; instanceId: string; error: string | null }
|
|
134
137
|
| { type: "SET_INITIALIZED" }
|
|
135
138
|
| { type: "TOGGLE_STATUS_BAR" }
|
|
136
139
|
| { type: "SET_THEME"; theme: string }
|
package/src/data/saved-copies.ts
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Answers
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Answers given from a copy saved earlier because refreshing it failed:
|
|
3
|
+
* offline, a hung or refused proxy, a service error.
|
|
4
|
+
*
|
|
5
|
+
* A CLI command answers once, so the line it ends with says "couldn't
|
|
6
|
+
* refresh; saved copy from <time>" instead of reading as live or delayed, but
|
|
7
|
+
* only when what it shows came from such a copy: a saved copy of something
|
|
8
|
+
* else it loaded on the way (a fallback for a pane it does not print) says
|
|
9
|
+
* nothing about the data on screen. The app says the same of a quote, in the
|
|
10
|
+
* footer of a pane showing one (`savedCopyAt`), until a refresh replaces it.
|
|
8
11
|
*
|
|
9
12
|
* The copy served is marked, by a symbol property that object spreads carry
|
|
10
13
|
* along and JSON never writes, and the command's output is searched for the
|
|
11
|
-
* marks.
|
|
12
|
-
*
|
|
13
|
-
* footers.
|
|
14
|
+
* marks. Outside a command only quotes are marked (`quoteServedFromSavedCopy`):
|
|
15
|
+
* the app keeps running for hours, and its other panes say how current their
|
|
16
|
+
* data is in their own footers. A quote built from a newer answer must not
|
|
17
|
+
* inherit a mark through a spread: `withSavedCopyMarkOf` gives it the mark of
|
|
18
|
+
* what was served instead.
|
|
14
19
|
*/
|
|
15
20
|
const SAVED_AT = Symbol("gloomberb.savedCopyAt");
|
|
16
21
|
|
|
17
22
|
let collecting = 0;
|
|
18
|
-
|
|
23
|
+
/** Whether anything was ever marked, so output is not searched for marks that cannot be there. */
|
|
24
|
+
let anyMarked = false;
|
|
19
25
|
|
|
20
26
|
type Marked = { [SAVED_AT]?: number };
|
|
21
27
|
|
|
22
28
|
function mark<T>(value: T, savedAt: number): T {
|
|
23
29
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
24
|
-
|
|
30
|
+
anyMarked = true;
|
|
25
31
|
return { ...value, [SAVED_AT]: savedAt };
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -36,13 +42,48 @@ export function servedFromSavedCopy<T>(value: T, savedAt: number): T {
|
|
|
36
42
|
return (Array.isArray(value) ? value.map((item) => mark(item, savedAt)) : mark(value, savedAt)) as T;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/** A quote answered from a copy saved at `savedAt` (ms) after its refresh failed: marked, in a command and in the app. */
|
|
46
|
+
export function quoteServedFromSavedCopy<T extends object>(quote: T, savedAt: number): T {
|
|
47
|
+
return Number.isFinite(savedAt) ? mark(quote, savedAt) : quote;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/**
|
|
40
51
|
* The saved-copy mark of `value`, to spread into what is drawn from it (a
|
|
41
52
|
* report row built from a quote's fields), or nothing when it has none.
|
|
42
53
|
*/
|
|
43
54
|
export function savedCopyMark(value: unknown): object {
|
|
55
|
+
const savedAt = savedCopyAt(value);
|
|
56
|
+
return savedAt != null ? { [SAVED_AT]: savedAt } : {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** When `value` itself was saved, if it is a saved copy served after a failed refresh; null otherwise. */
|
|
60
|
+
export function savedCopyAt(value: unknown): number | null {
|
|
44
61
|
const savedAt = value && typeof value === "object" ? (value as Marked)[SAVED_AT] : undefined;
|
|
45
|
-
return typeof savedAt === "number" ?
|
|
62
|
+
return typeof savedAt === "number" ? savedAt : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The oldest saved copy among `values` (a pane's quotes), by when it was saved; null when none is one. */
|
|
66
|
+
export function oldestSavedCopyAt(values: Iterable<unknown>): number | null {
|
|
67
|
+
let oldest: number | null = null;
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
const savedAt = savedCopyAt(value);
|
|
70
|
+
if (savedAt != null) oldest = Math.min(oldest ?? savedAt, savedAt);
|
|
71
|
+
}
|
|
72
|
+
return oldest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `value` carrying the saved-copy mark of `served` and no other: what is
|
|
77
|
+
* built from an answer (resolved, merged onto an older quote) is a saved copy
|
|
78
|
+
* exactly when that answer was.
|
|
79
|
+
*/
|
|
80
|
+
export function withSavedCopyMarkOf<T extends object>(value: T, served: unknown): T {
|
|
81
|
+
const savedAt = savedCopyAt(served);
|
|
82
|
+
const own = savedCopyAt(value);
|
|
83
|
+
if (own === savedAt) return value;
|
|
84
|
+
if (savedAt != null) return mark(value, savedAt);
|
|
85
|
+
const { [SAVED_AT]: _dropped, ...rest } = value as T & Marked;
|
|
86
|
+
return rest as T;
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
/** How far into a command's output the marks are looked for, so a large report is not walked whole. */
|
|
@@ -55,7 +96,7 @@ const MAX_NODES = 50_000;
|
|
|
55
96
|
* came from one. The oldest, since that is how old what is shown can be.
|
|
56
97
|
*/
|
|
57
98
|
export function savedCopyShownAt(shown: unknown): number | null {
|
|
58
|
-
if (collecting === 0 ||
|
|
99
|
+
if (collecting === 0 || !anyMarked) return null;
|
|
59
100
|
let oldest: number | null = null;
|
|
60
101
|
let budget = MAX_NODES;
|
|
61
102
|
const seen = new Set<object>();
|
|
@@ -71,14 +112,12 @@ export function savedCopyShownAt(shown: unknown): number | null {
|
|
|
71
112
|
return oldest;
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
/** Runs one command,
|
|
115
|
+
/** Runs one command, whose output `savedCopyShownAt` then searches for saved copies. */
|
|
75
116
|
export async function collectSavedCopies<T>(run: () => Promise<T>): Promise<T> {
|
|
76
|
-
if (collecting === 0) marked = 0;
|
|
77
117
|
collecting += 1;
|
|
78
118
|
try {
|
|
79
119
|
return await run();
|
|
80
120
|
} finally {
|
|
81
121
|
collecting -= 1;
|
|
82
|
-
if (collecting === 0) marked = 0;
|
|
83
122
|
}
|
|
84
123
|
}
|