gloomberb 0.17.4 → 0.17.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/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/data.ts +4 -1
- package/src/api-client/research-activity.ts +10 -3
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/update.ts +50 -3
- package/src/app.tsx +2 -0
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/glossary.ts +34 -4
- package/src/cli/index.ts +6 -0
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/components/chart/composite/format.ts +7 -0
- package/src/components/chart/composite/legend.tsx +8 -11
- package/src/components/command-bar/commands/direct/index.ts +47 -0
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +16 -1
- package/src/components/command-bar/surface/index.tsx +2 -1
- package/src/components/command-bar/workflow/ops.ts +4 -3
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/fields.tsx +5 -0
- package/src/data/config/store/normalize.ts +8 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-cn.ts +2 -0
- package/src/i18n/zh-tw.ts +2 -0
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +4 -0
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/hooks.tsx +168 -29
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +20 -0
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/state.ts +15 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +2 -0
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/futures-curve/headless.ts +13 -6
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +64 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/options-positioning/model.ts +14 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
- package/src/renderers/electrobun/shared/protocol.ts +3 -2
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/time-series/resolve.ts +21 -6
- package/src/time-series/spec.ts +8 -0
- package/src/types/config.ts +14 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +125 -16
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/semver.ts +47 -2
|
@@ -250,6 +250,14 @@ export const commands: Command[] = [
|
|
|
250
250
|
description: "Check GitHub releases for a newer version",
|
|
251
251
|
category: "Config",
|
|
252
252
|
},
|
|
253
|
+
{
|
|
254
|
+
// Not "Update ...": "update" has to land on Check for Updates first.
|
|
255
|
+
id: "release-channel",
|
|
256
|
+
prefix: "",
|
|
257
|
+
label: "Release Channel",
|
|
258
|
+
description: "Update from stable releases, or from nightly builds as well",
|
|
259
|
+
category: "Config",
|
|
260
|
+
},
|
|
253
261
|
{
|
|
254
262
|
id: "toggle-crash-reports",
|
|
255
263
|
prefix: "",
|
|
@@ -52,3 +52,11 @@ 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
|
+
}
|
|
@@ -4,6 +4,9 @@ import { isManualPortfolio } from "../../../../plugins/builtin/portfolio-list/mu
|
|
|
4
4
|
import type { Command } from "../../commands/registry";
|
|
5
5
|
import type { ResultItem } from "../../list/model";
|
|
6
6
|
import { canRestartToApply } from "../../../../updater";
|
|
7
|
+
import { NIGHTLY_CHANNEL_SUMMARY, resolveUpdateChannel } from "../../../../updater/channel";
|
|
8
|
+
import { isPrereleaseVersion } from "../../../../utils/semver";
|
|
9
|
+
import { VERSION } from "../../../../version";
|
|
7
10
|
|
|
8
11
|
interface RootCommandItemBuilderOptions {
|
|
9
12
|
activeCollectionId: string | null;
|
|
@@ -157,6 +160,11 @@ export function createRootCommandItemBuilder({
|
|
|
157
160
|
return state.config.telemetry?.attention === true
|
|
158
161
|
? "Currently on: share ticker research counts"
|
|
159
162
|
: "Currently off: opt in to sharing ticker research counts";
|
|
163
|
+
case "release-channel":
|
|
164
|
+
if (resolveUpdateChannel(state.config) === "nightly") return `Currently nightly: ${NIGHTLY_CHANNEL_SUMMARY}`;
|
|
165
|
+
return isPrereleaseVersion(VERSION)
|
|
166
|
+
? "Currently stable: this nightly build stays until a newer stable release"
|
|
167
|
+
: "Currently stable";
|
|
160
168
|
case "font-size-increase":
|
|
161
169
|
case "font-size-decrease":
|
|
162
170
|
return `Currently ${state.config.fontSize ?? 12}px`;
|
|
@@ -176,6 +184,8 @@ export function createRootCommandItemBuilder({
|
|
|
176
184
|
return state.config.telemetry?.usage === false ? "off" : "on";
|
|
177
185
|
case "toggle-attention-counts":
|
|
178
186
|
return state.config.telemetry?.attention === true ? "on" : "off";
|
|
187
|
+
case "release-channel":
|
|
188
|
+
return resolveUpdateChannel(state.config);
|
|
179
189
|
default:
|
|
180
190
|
return command.prefix || undefined;
|
|
181
191
|
}
|
|
@@ -185,8 +195,13 @@ export function createRootCommandItemBuilder({
|
|
|
185
195
|
switch (command.id) {
|
|
186
196
|
case "set-portfolio-position":
|
|
187
197
|
return "edit position update position modify position manual position portfolio position";
|
|
198
|
+
// "update" is a whole word here so it lands on this command before Set
|
|
199
|
+
// Portfolio Position (whose keywords say "update position") and Release
|
|
200
|
+
// Channel, which keeps the word further back.
|
|
188
201
|
case "check-for-updates":
|
|
189
|
-
return state.updateProgress?.phase === "ready" ? "check for updates restart relaunch" : "";
|
|
202
|
+
return state.updateProgress?.phase === "ready" ? "update check for updates restart relaunch" : "update";
|
|
203
|
+
case "release-channel":
|
|
204
|
+
return "nightly stable beta prerelease update updates channel";
|
|
190
205
|
default:
|
|
191
206
|
return "";
|
|
192
207
|
}
|
|
@@ -77,9 +77,10 @@ export function CommandBar({
|
|
|
77
77
|
titleBarOverlay,
|
|
78
78
|
visibleListStateRef,
|
|
79
79
|
} = useCommandBarEnvironment();
|
|
80
|
+
// Without an updater (the web terminal) neither checking nor a channel means anything.
|
|
80
81
|
const availableCommands = useMemo(() => onCheckForUpdates
|
|
81
82
|
? allAvailableCommands
|
|
82
|
-
: allAvailableCommands.filter((command) => command.id !== "check-for-updates"), [allAvailableCommands, onCheckForUpdates]);
|
|
83
|
+
: allAvailableCommands.filter((command) => command.id !== "check-for-updates" && command.id !== "release-channel"), [allAvailableCommands, onCheckForUpdates]);
|
|
83
84
|
const {
|
|
84
85
|
applyThemePreview,
|
|
85
86
|
clearThemePreview,
|
|
@@ -2,7 +2,7 @@ import { type LayoutConfig, type PaneBinding, type PaneInstanceConfig } from "..
|
|
|
2
2
|
import type { PaneSettingField, PaneTemplateContext, PaneTemplateCreateOptions, PaneTemplateInstanceConfig, PaneTemplateDef } from "../../../types/plugin";
|
|
3
3
|
import { getFocusedCollectionId, getFocusedTickerSymbol } from "../../../state/app/context";
|
|
4
4
|
import { resolveInstrumentForPane, resolveListingForPane } from "../../../core/state/app/instrument";
|
|
5
|
-
import { resolveTickerOpenTarget } from "../../../tickers/open-target";
|
|
5
|
+
import { resolveTickerOpenTarget, tickerOpenFailureMessage } from "../../../tickers/open-target";
|
|
6
6
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
7
7
|
import { scopedBrokerContractIdentityKey } from "../../../utils/instrument-identity";
|
|
8
8
|
import { tickerSelectionFromSearchResult } from "../../../tickers/selection";
|
|
@@ -120,9 +120,10 @@ async function resolvePaneTemplateOptions(
|
|
|
120
120
|
&& (tickerNamed || !template.shortcut.argOptional);
|
|
121
121
|
if (resolvesTicker) {
|
|
122
122
|
if (resolvedOptions?.instrument === null && !resolvedOptions.ticker) {
|
|
123
|
-
const
|
|
123
|
+
const publicQuery = resolvedOptions.symbol ?? resolvedOptions.arg ?? baseContext.activeTicker ?? "";
|
|
124
|
+
const publicTarget = await resolveTickerOpenTarget({ query: publicQuery,
|
|
124
125
|
tickers: state.tickers, tickerRepository: deps.tickerRepository, dataProvider: deps.dataProvider, publicOnly: true });
|
|
125
|
-
if (!publicTarget) throw new Error(
|
|
126
|
+
if (!publicTarget) throw new Error(tickerOpenFailureMessage(publicQuery.trim().toUpperCase()));
|
|
126
127
|
deps.dispatch({ type: "UPDATE_TICKER", ticker: publicTarget.ticker });
|
|
127
128
|
if (publicTarget.created) deps.pluginRegistry.events.emit("ticker:added", { symbol: publicTarget.symbol, ticker: publicTarget.ticker });
|
|
128
129
|
resolvedOptions = { ...resolvedOptions, symbol: publicTarget.symbol, ticker: publicTarget.ticker, listing: resolvedOptions.listing ?? publicTarget.listing };
|
|
@@ -137,6 +137,7 @@ export function ConfirmModalContent({
|
|
|
137
137
|
confirmLabel={t(confirm.confirmLabel)}
|
|
138
138
|
cancelLabel={t(confirmCancelLabel(confirm.cancelLabel))}
|
|
139
139
|
confirmVariant={confirm.tone === "default" ? "primary" : "danger"}
|
|
140
|
+
defaultAction={confirm.defaultAction}
|
|
140
141
|
width={contentWidth}
|
|
141
142
|
status={status}
|
|
142
143
|
busy={pending}
|
|
@@ -12,6 +12,8 @@ export interface ConfirmModalOptions {
|
|
|
12
12
|
cancelLabel?: string;
|
|
13
13
|
/** Danger unless said otherwise: most confirms guard a deletion. */
|
|
14
14
|
tone?: "default" | "danger";
|
|
15
|
+
/** What Enter answers: `cancel` for an opt-in a stray Enter must not accept (ConfirmDialog). */
|
|
16
|
+
defaultAction?: "confirm" | "cancel";
|
|
15
17
|
/** Reads what it acts on when it runs, not when the confirm opened. */
|
|
16
18
|
onConfirm: () => void | Promise<void>;
|
|
17
19
|
/** "stay" keeps the confirm open after it ran; "back" closes it, as "close" does. */
|
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
paneManagementAccelerators,
|
|
39
39
|
resolvePaneManagementShortcut,
|
|
40
40
|
} from "./shell/shortcuts";
|
|
41
|
-
import { menuItemsForFallback, paneFooterMenuItems } from "./shell/menu";
|
|
41
|
+
import { menuItemsForFallback, paneFooterMenuItems, REPORT_BUG_MENU_LABEL } from "./shell/menu";
|
|
42
|
+
import { canReportPaneBug } from "../../shares/bug-report-flow";
|
|
42
43
|
import { useShellPaneCapture } from "./shell/pane/capture";
|
|
43
44
|
import { captureStampFooter, usePaneCaptureStamp } from "./pane/capture-stamp";
|
|
44
45
|
import { pluginContextMenuSlot, withPluginContextMenuItems } from "../../ui/context-menu";
|
|
@@ -252,6 +253,10 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
252
253
|
}
|
|
253
254
|
items.push({ id: "pane-docs", label: "Docs", onSelect: openDocs });
|
|
254
255
|
items.push({ id: "share-pane", label: "Share", accelerator: accelerators.share, onSelect: sharePane });
|
|
256
|
+
// A popped-out window paints its pane's DOM, so it can always picture it.
|
|
257
|
+
if (canReportPaneBug(true)) {
|
|
258
|
+
items.push({ id: "report-bug", label: REPORT_BUG_MENU_LABEL, onSelect: () => pluginRegistry.reportPaneBug(desktopWindowBridge.paneId) });
|
|
259
|
+
}
|
|
255
260
|
items.push(pluginContextMenuSlot("pane"));
|
|
256
261
|
if (dockPane) {
|
|
257
262
|
items.push({ id: "dock", label: "Dock in Main Window", accelerator: accelerators.toggleFloating, onSelect: dockPane });
|
|
@@ -178,9 +178,11 @@ export function PaneSidebar({
|
|
|
178
178
|
(resize?.onResizeEnd ?? resize?.onResize)?.(next);
|
|
179
179
|
}, [resize, resolveDragWidth, stopLiveDrag]);
|
|
180
180
|
|
|
181
|
-
// The keyboard moves the divider too: `<` and `>` while the pane is focused
|
|
182
|
-
//
|
|
183
|
-
//
|
|
181
|
+
// The keyboard moves the divider too: `<` and `>` while the pane is focused.
|
|
182
|
+
// They are bound and listed in the pane menu, not drawn in the footer: the
|
|
183
|
+
// width of a list is not one of the pane's actions, and the divider shows
|
|
184
|
+
// where it is (on the desktop its tooltip names the keys). Each press is a
|
|
185
|
+
// finished resize, so it persists.
|
|
184
186
|
const resizeRegistrationId = `pane-sidebar-resize:${useId()}`;
|
|
185
187
|
const canNarrow = !!resize && width > resize.min;
|
|
186
188
|
const canWiden = !!resize && width < resize.max;
|
|
@@ -189,14 +191,25 @@ export function PaneSidebar({
|
|
|
189
191
|
const next = Math.min(resize.max, Math.max(resize.min, width + delta));
|
|
190
192
|
if (next !== width) (resize.onResizeEnd ?? resize.onResize)(next);
|
|
191
193
|
};
|
|
192
|
-
usePaneFooter(resizeRegistrationId, () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{ id: "sidebar-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
usePaneFooter(resizeRegistrationId, () => {
|
|
195
|
+
if (!resize) return null;
|
|
196
|
+
const steps = [
|
|
197
|
+
{ id: "sidebar-narrow", key: NARROW_KEY, title: "Narrow Sidebar", delta: -KEYBOARD_RESIZE_STEP, disabled: !canNarrow },
|
|
198
|
+
{ id: "sidebar-widen", key: WIDEN_KEY, title: "Widen Sidebar", delta: KEYBOARD_RESIZE_STEP, disabled: !canWiden },
|
|
199
|
+
];
|
|
200
|
+
return {
|
|
201
|
+
// After the pane's own actions, which register at order 0.
|
|
202
|
+
order: 1,
|
|
203
|
+
keys: steps.map(({ id, key, delta, disabled }) => ({ id, key, label: "", onPress: () => stepResize(delta), disabled })),
|
|
204
|
+
// A bound that is reached drops out, as a disabled footer hint did.
|
|
205
|
+
menu: steps.filter((step) => !step.disabled).map(({ id, key, title, delta }) => ({
|
|
206
|
+
id: `pane-sidebar:${id}`,
|
|
207
|
+
label: title,
|
|
208
|
+
accelerator: key,
|
|
209
|
+
onSelect: () => stepResize(delta),
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}, [!!resize, canNarrow, canWiden, resize?.min, resize?.max, width]);
|
|
200
213
|
|
|
201
214
|
const borderWidth = nativePaneChrome ? 0 : width > 1 ? 1 : 0;
|
|
202
215
|
const listWidth = Math.max(width - borderWidth, 1);
|
|
@@ -71,6 +71,7 @@ import type { AppTickerRepositoryPort } from "../../../core/app-service-ports";
|
|
|
71
71
|
import type { DataProvider } from "../../../types/data-provider";
|
|
72
72
|
import { useShellPaneActions } from "./pane/actions";
|
|
73
73
|
import { useShellPaneCapture } from "./pane/capture";
|
|
74
|
+
import { canReportPaneBug } from "../../../shares/bug-report-flow";
|
|
74
75
|
import { useShellPaneLayoutMoves } from "./pane/new-layout";
|
|
75
76
|
import { resolvePaneFocusSourceLayout } from "./fullscreen";
|
|
76
77
|
import { useTransientLayout } from "../transient-layout";
|
|
@@ -668,6 +669,9 @@ export function Shell({
|
|
|
668
669
|
paneLayoutMoveMenu(paneId),
|
|
669
670
|
() => { openPaneDocs(paneId); },
|
|
670
671
|
canAsk ? () => { askAboutPane(paneId, { cover: kitMenuRectRef.current }); } : undefined,
|
|
672
|
+
canReportPaneBug(nativePaneChrome || !!nativeRenderer.capturePaneImage)
|
|
673
|
+
? () => { pluginRegistry.reportPaneBug(paneId); }
|
|
674
|
+
: undefined,
|
|
671
675
|
);
|
|
672
676
|
const showKitMenu = () => {
|
|
673
677
|
const fallbackSourceItems = withPluginContextMenuItems(context, items, pluginRegistry);
|
|
@@ -710,7 +714,7 @@ export function Shell({
|
|
|
710
714
|
void showContextMenu(context, items, event).then((shown) => {
|
|
711
715
|
if (!shown) showKitMenu();
|
|
712
716
|
});
|
|
713
|
-
}, [askAboutPane, canAsk, canExportPaneCsv, closePaneMenu, contentHeight, copyPaneScreenshot, desktopWindowBridge, exportPaneCsv, focusPane, getPaneTitle, handlePaneQuickSetting, nativePaneChrome, togglePaneFullscreen, openPaneDocs, openPaneSettings, paneAccelerators, paneLayoutMoveMenu, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, sharePane, shortcutDisplayMode, showContextMenu, titleState, visibleLayout, width]);
|
|
717
|
+
}, [askAboutPane, canAsk, canExportPaneCsv, closePaneMenu, contentHeight, copyPaneScreenshot, desktopWindowBridge, exportPaneCsv, focusPane, getPaneTitle, handlePaneQuickSetting, nativePaneChrome, nativeRenderer, togglePaneFullscreen, openPaneDocs, openPaneSettings, paneAccelerators, paneLayoutMoveMenu, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, sharePane, shortcutDisplayMode, showContextMenu, titleState, visibleLayout, width]);
|
|
714
718
|
openPaneMenuRef.current = openPaneMenu;
|
|
715
719
|
|
|
716
720
|
|
|
@@ -25,6 +25,9 @@ const MENU_MAX_WIDTH = 44;
|
|
|
25
25
|
|
|
26
26
|
export const MENU_Z_INDEX = 10_000;
|
|
27
27
|
|
|
28
|
+
/** The pane menu entry that opens Report a Bug, in the main window's and a popped-out window's menu. */
|
|
29
|
+
export const REPORT_BUG_MENU_LABEL = "Report a Bug";
|
|
30
|
+
|
|
28
31
|
/** One row of the kit pane menu; a divider row is skipped by the keyboard. */
|
|
29
32
|
export interface PaneMenuEntry {
|
|
30
33
|
id: string;
|
|
@@ -107,6 +110,8 @@ export function menuForPane(
|
|
|
107
110
|
openDocs?: () => void,
|
|
108
111
|
/** A new Ask Gloom conversation with this pane attached, the same as the button beside the `?`. */
|
|
109
112
|
askGloom?: () => void,
|
|
113
|
+
/** Report a Bug: the pane's picture and a line to #bugs; absent where it cannot be posted or pictured. */
|
|
114
|
+
reportBug?: () => void,
|
|
110
115
|
): ContextMenuItem[] {
|
|
111
116
|
const baseActions: ContextMenuItem[] = [...paneItems];
|
|
112
117
|
if (baseActions.length > 0) baseActions.push(contextMenuDivider("pane:own-actions-divider"));
|
|
@@ -141,6 +146,9 @@ export function menuForPane(
|
|
|
141
146
|
onSelect: sharePane,
|
|
142
147
|
});
|
|
143
148
|
}
|
|
149
|
+
if (reportBug) {
|
|
150
|
+
baseActions.push({ id: "report-bug", label: REPORT_BUG_MENU_LABEL, onSelect: reportBug });
|
|
151
|
+
}
|
|
144
152
|
if (copyPaneScreenshot) {
|
|
145
153
|
baseActions.push({
|
|
146
154
|
id: "copy-screenshot",
|
|
@@ -4,13 +4,14 @@ import type { LayoutBounds, ResolvedPane } from "../../../../layout/pane-manager
|
|
|
4
4
|
import type { PluginRegistry } from "../../../../plugins/registry";
|
|
5
5
|
import { paneScreenshotScale, PANE_SCREENSHOT_MIN_WIDTH } from "../../../../remote/pane-screenshot";
|
|
6
6
|
import { base64Bytes, formatCaptureStamp, type PaneImageCapture } from "../../../../shares/pane-image";
|
|
7
|
-
import {
|
|
7
|
+
import { capturedPersonalData } from "../../../../shares/personal-pane";
|
|
8
8
|
import { resolveTickerForPane, useAppStateRef } from "../../../../state/app/context";
|
|
9
9
|
import type { LayoutConfig } from "../../../../types/config";
|
|
10
10
|
import { useUiCapabilities, type NativeRendererHost } from "../../../../ui";
|
|
11
11
|
import { buildPaneSharePayload } from "../../../../shares/pane";
|
|
12
12
|
import { preparePaneShare } from "../../../../shares/prepare";
|
|
13
13
|
import { shareFocusedPane } from "../../../../shares/flow";
|
|
14
|
+
import { reportPaneBug } from "../../../../shares/bug-report-flow";
|
|
14
15
|
import { useDialog } from "../../../../ui/dialog";
|
|
15
16
|
import { capturePaneImage as paintPaneImage } from "../../../../utils/dom-screenshot";
|
|
16
17
|
import { setPaneCaptureStamp } from "../../pane/capture-stamp";
|
|
@@ -45,14 +46,16 @@ interface UseShellPaneCaptureOptions {
|
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Binds `capturePaneImage` and `promptDialog` for plugins that post a pane's
|
|
48
|
-
* picture or copy its link
|
|
49
|
-
*
|
|
49
|
+
* picture or copy its link, and Share and Report a Bug, which are built on
|
|
50
|
+
* them. The desktop and the web paint the pane's DOM; the terminal draws the
|
|
51
|
+
* cells it shows, kitty charts included.
|
|
50
52
|
*/
|
|
51
53
|
export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
|
|
52
54
|
const { pluginRegistry } = options;
|
|
53
55
|
const dialog = useDialog();
|
|
54
|
-
// Shared by menu, command, key
|
|
55
|
-
|
|
56
|
+
// Shared by Share and Report a Bug from a menu, command, key or plugin call,
|
|
57
|
+
// including triggers without a repeat flag: one capture flow at a time.
|
|
58
|
+
const [captureOnce] = useState(createOneShot);
|
|
56
59
|
const { publicSharing } = useUiCapabilities();
|
|
57
60
|
const stateRef = useAppStateRef();
|
|
58
61
|
const latest = useRef(options);
|
|
@@ -71,6 +74,8 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
|
|
|
71
74
|
setPaneCaptureStamp(paneId, formatCaptureStamp(capturedAt));
|
|
72
75
|
try {
|
|
73
76
|
let image: Pick<PaneImageCapture, "png" | "width" | "height" | "pixels" | "cells">;
|
|
77
|
+
// The terminal's picture is the screen's cells, so it shows whatever is drawn over the pane.
|
|
78
|
+
let drawn: Map<string, LayoutBounds> | null = null;
|
|
74
79
|
if (current.nativePaneChrome) {
|
|
75
80
|
await nextFrames(2);
|
|
76
81
|
const shot = await paintPaneImage({
|
|
@@ -81,7 +86,8 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
|
|
|
81
86
|
});
|
|
82
87
|
image = { png: base64Bytes(shot.pngBase64), width: shot.width, height: shot.height };
|
|
83
88
|
} else {
|
|
84
|
-
|
|
89
|
+
drawn = current.screenRects();
|
|
90
|
+
const rect = drawn.get(paneId);
|
|
85
91
|
if (!rect || !current.nativeRenderer.capturePaneImage) {
|
|
86
92
|
throw new Error("Show the pane on screen to share it.");
|
|
87
93
|
}
|
|
@@ -106,11 +112,13 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
|
|
|
106
112
|
resolveTickerForPane(stateRef.current, paneId),
|
|
107
113
|
stateRef.current.tickers,
|
|
108
114
|
) : null,
|
|
109
|
-
personalData:
|
|
115
|
+
personalData: capturedPersonalData({
|
|
110
116
|
registry: current.pluginRegistry,
|
|
111
117
|
state: stateRef.current,
|
|
112
118
|
layout: current.layout,
|
|
113
|
-
|
|
119
|
+
paneId,
|
|
120
|
+
panes: current.paneMap,
|
|
121
|
+
drawn,
|
|
114
122
|
}),
|
|
115
123
|
...image,
|
|
116
124
|
};
|
|
@@ -122,15 +130,24 @@ export function useShellPaneCapture(options: UseShellPaneCaptureOptions): void {
|
|
|
122
130
|
useEffect(() => pluginRegistry.bindHost({
|
|
123
131
|
sharePane: (requestedPaneId) => {
|
|
124
132
|
const paneId = requestedPaneId ?? latest.current.focusedPaneId;
|
|
125
|
-
if (!paneId ||
|
|
126
|
-
|
|
133
|
+
if (!paneId || captureOnce.refuses()) return;
|
|
134
|
+
captureOnce.start();
|
|
127
135
|
// Let a menu/command close before the capture starts. The flow also refuses
|
|
128
136
|
// a second run for the entire dialog/post, even beyond the cooldown's timeout.
|
|
129
137
|
setTimeout(() => {
|
|
130
|
-
void shareFocusedPane(pluginRegistry, paneId).finally(() =>
|
|
138
|
+
void shareFocusedPane(pluginRegistry, paneId).finally(() => captureOnce.end());
|
|
139
|
+
}, 0);
|
|
140
|
+
},
|
|
141
|
+
reportPaneBug: (requestedPaneId) => {
|
|
142
|
+
const paneId = requestedPaneId ?? latest.current.focusedPaneId;
|
|
143
|
+
if (!paneId || captureOnce.refuses()) return;
|
|
144
|
+
captureOnce.start();
|
|
145
|
+
// After the menu that chose it has closed.
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
void reportPaneBug(pluginRegistry, paneId).finally(() => captureOnce.end());
|
|
131
148
|
}, 0);
|
|
132
149
|
},
|
|
133
150
|
capturePaneImage: capture,
|
|
134
151
|
promptDialog: (prompt) => dialog.prompt(prompt),
|
|
135
|
-
}), [capture, dialog, pluginRegistry
|
|
152
|
+
}), [capture, captureOnce, dialog, pluginRegistry]);
|
|
136
153
|
}
|
|
@@ -36,6 +36,12 @@ export interface ConfirmDialogProps extends PromptContext<boolean> {
|
|
|
36
36
|
* opens on its own while the person may be typing. Esc and `n` always answer.
|
|
37
37
|
*/
|
|
38
38
|
keyGraceMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* What Enter answers. `cancel` is for an opt-in a stray Enter must not
|
|
41
|
+
* accept: Enter takes the cancel button, which is drawn as the primary one,
|
|
42
|
+
* and only `y` or the confirm button confirms.
|
|
43
|
+
*/
|
|
44
|
+
defaultAction?: "confirm" | "cancel";
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export function ConfirmDialog({
|
|
@@ -52,7 +58,9 @@ export function ConfirmDialog({
|
|
|
52
58
|
onClose,
|
|
53
59
|
formButtons = false,
|
|
54
60
|
keyGraceMs = 0,
|
|
61
|
+
defaultAction = "confirm",
|
|
55
62
|
}: ConfirmDialogProps) {
|
|
63
|
+
const enterCancels = defaultAction === "cancel";
|
|
56
64
|
const desktop = useUiHost().kind === "desktop-web";
|
|
57
65
|
const openedAt = useRef(Date.now());
|
|
58
66
|
const confirm = useCallback(() => resolve(true), [resolve]);
|
|
@@ -71,7 +79,13 @@ export function ConfirmDialog({
|
|
|
71
79
|
event.preventDefault();
|
|
72
80
|
return;
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
const enter = event.name === "enter" || event.name === "return";
|
|
83
|
+
if (enter && enterCancels) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
cancel();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (enter || isPlainKey(event, "y")) {
|
|
75
89
|
event.preventDefault();
|
|
76
90
|
if (Date.now() - openedAt.current < keyGraceMs) return;
|
|
77
91
|
confirm();
|
|
@@ -88,13 +102,25 @@ export function ConfirmDialog({
|
|
|
88
102
|
|
|
89
103
|
// The desktop shows the keys on the buttons instead of a footer line; a
|
|
90
104
|
// confirm with no key line keeps its buttons plain there too.
|
|
105
|
+
// One primary button, the one Enter answers.
|
|
91
106
|
const keys = footer !== "";
|
|
92
|
-
const confirmButton =
|
|
93
|
-
|
|
107
|
+
const confirmButton = (
|
|
108
|
+
<Button
|
|
109
|
+
label={confirmLabel}
|
|
110
|
+
variant={enterCancels && confirmVariant === "primary" ? "secondary" : confirmVariant}
|
|
111
|
+
disabled={busy}
|
|
112
|
+
desktopShortcut={keys ? (enterCancels ? "Y" : "Enter") : undefined}
|
|
113
|
+
onPress={confirm}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
const cancelButton = <Button label={cancelLabel} variant={enterCancels ? "primary" : "secondary"} disabled={busy} desktopShortcut={keys ? (enterCancels ? "Enter" : "Esc") : undefined} onPress={cancel} />;
|
|
117
|
+
const keyLine = enterCancels
|
|
118
|
+
? tf("y {action} · Enter or Esc {cancel}", { action: t(confirmLabel).toLowerCase(), cancel: t(cancelLabel).toLowerCase() })
|
|
119
|
+
: tf("Enter {action} · Esc cancel", { action: t(confirmLabel).toLowerCase() });
|
|
94
120
|
return (
|
|
95
121
|
<DialogFrame
|
|
96
122
|
title={title}
|
|
97
|
-
footer={footer ??
|
|
123
|
+
footer={footer ?? keyLine}
|
|
98
124
|
desktopFooter={null}
|
|
99
125
|
onClose={onClose}
|
|
100
126
|
>
|
|
@@ -48,6 +48,8 @@ export interface TextFieldProps {
|
|
|
48
48
|
labelWidth?: number;
|
|
49
49
|
value?: string;
|
|
50
50
|
placeholder?: string;
|
|
51
|
+
/** The most characters it takes; typing and pasting stop there. */
|
|
52
|
+
maxLength?: number;
|
|
51
53
|
focused?: boolean;
|
|
52
54
|
width?: number;
|
|
53
55
|
inputRef?: RefObject<InputRenderable | null>;
|
|
@@ -119,6 +121,7 @@ function TextFieldControl({
|
|
|
119
121
|
active,
|
|
120
122
|
value,
|
|
121
123
|
placeholder,
|
|
124
|
+
maxLength,
|
|
122
125
|
focused,
|
|
123
126
|
width,
|
|
124
127
|
inputRef,
|
|
@@ -163,6 +166,7 @@ function TextFieldControl({
|
|
|
163
166
|
label={label}
|
|
164
167
|
value={value}
|
|
165
168
|
placeholder={placeholder}
|
|
169
|
+
maxLength={maxLength}
|
|
166
170
|
focused={focused}
|
|
167
171
|
width={width}
|
|
168
172
|
inputRef={inputRef}
|
|
@@ -226,6 +230,7 @@ function TextFieldControl({
|
|
|
226
230
|
value={value}
|
|
227
231
|
selectable={!isPassword}
|
|
228
232
|
placeholder={isPassword ? "" : placeholder}
|
|
233
|
+
{...(maxLength === undefined ? {} : { maxLength })}
|
|
229
234
|
focused={focused}
|
|
230
235
|
textColor={isPassword ? backgroundColor : textColor}
|
|
231
236
|
placeholderColor={placeholderColor}
|
|
@@ -27,6 +27,7 @@ import { isRecord } from "../../../utils/guards";
|
|
|
27
27
|
import { debugLog } from "../../../utils/debug-log";
|
|
28
28
|
import { DEFAULT_THEME, themes } from "../../../theme/themes";
|
|
29
29
|
import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
|
|
30
|
+
import { sanitizeUpdatesConfig } from "../../../updater/channel";
|
|
30
31
|
|
|
31
32
|
const configLog = debugLog.createLogger("config");
|
|
32
33
|
const reportedUnknownThemes = new Set<string>();
|
|
@@ -106,6 +107,7 @@ export function normalizeLoadedConfig(
|
|
|
106
107
|
...withKeybindings(sanitizeKeybindings(candidate.keybindings)),
|
|
107
108
|
...withTelemetry(sanitizeTelemetry(candidate.telemetry)),
|
|
108
109
|
...withStarPrompt(sanitizeStarPrompt(candidate.starPrompt)),
|
|
110
|
+
...withUpdates(sanitizeUpdatesConfig(candidate.updates)),
|
|
109
111
|
};
|
|
110
112
|
|
|
111
113
|
const needsSave =
|
|
@@ -169,6 +171,8 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
|
|
|
169
171
|
Object.assign(persisted, withTelemetry(sanitizeTelemetry(config.telemetry)));
|
|
170
172
|
delete persisted.starPrompt;
|
|
171
173
|
Object.assign(persisted, withStarPrompt(sanitizeStarPrompt(config.starPrompt)));
|
|
174
|
+
delete persisted.updates;
|
|
175
|
+
Object.assign(persisted, withUpdates(sanitizeUpdatesConfig(config.updates)));
|
|
172
176
|
|
|
173
177
|
return persisted;
|
|
174
178
|
}
|
|
@@ -185,6 +189,10 @@ function withStarPrompt(starPrompt: AppConfig["starPrompt"]): Pick<AppConfig, "s
|
|
|
185
189
|
return starPrompt ? { starPrompt } : {};
|
|
186
190
|
}
|
|
187
191
|
|
|
192
|
+
function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates"> {
|
|
193
|
+
return updates ? { updates } : {};
|
|
194
|
+
}
|
|
195
|
+
|
|
188
196
|
/** Only the switches that are set survive; an empty object is the same as none. */
|
|
189
197
|
function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
|
|
190
198
|
if (!isRecord(value)) return undefined;
|
package/src/i18n/es.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const es: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "No se pudo guardar este valor.",
|
|
71
71
|
"Could not apply that setting.": "No se pudo aplicar ese ajuste.",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc cancelar",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter o Esc {cancel}",
|
|
73
74
|
"Published {date}": "Publicado {date}",
|
|
74
75
|
"Language": "Idioma",
|
|
75
76
|
"Unsupported language": "Idioma no compatible",
|
|
@@ -151,6 +152,7 @@ export const es: Record<string, string> = {
|
|
|
151
152
|
"Toggle Status Bar": "Alternar barra de estado",
|
|
152
153
|
"Toggle Value Flashing": "Alternar parpadeo de valores",
|
|
153
154
|
"Check for Updates": "Buscar actualizaciones",
|
|
155
|
+
"Release Channel": "Canal de versiones",
|
|
154
156
|
"Crash Reports": "Informes de fallos",
|
|
155
157
|
"Usage Counts": "Recuento de uso",
|
|
156
158
|
"Change Theme": "Cambiar tema",
|
package/src/i18n/ja.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const ja: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "この値を保存できませんでした。",
|
|
71
71
|
"Could not apply that setting.": "その設定を適用できませんでした。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc キャンセル",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter または Esc {cancel}",
|
|
73
74
|
"Published {date}": "{date} 公開",
|
|
74
75
|
"Language": "言語",
|
|
75
76
|
"Unsupported language": "対応していない言語",
|
|
@@ -152,6 +153,7 @@ export const ja: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "ステータスバーを切り替え",
|
|
153
154
|
"Toggle Value Flashing": "値の点滅を切り替え",
|
|
154
155
|
"Check for Updates": "アップデートを確認",
|
|
156
|
+
"Release Channel": "リリースチャネル",
|
|
155
157
|
"Crash Reports": "クラッシュレポート",
|
|
156
158
|
"Usage Counts": "利用回数",
|
|
157
159
|
"Change Theme": "テーマを変更",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -68,6 +68,7 @@ export const ko: Record<string, string> = {
|
|
|
68
68
|
"Could not save this value.": "이 값을 저장할 수 없습니다.",
|
|
69
69
|
"Could not apply that setting.": "해당 설정을 적용할 수 없습니다.",
|
|
70
70
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 취소",
|
|
71
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 또는 Esc {cancel}",
|
|
71
72
|
"Published {date}": "게시일 {date}",
|
|
72
73
|
"Language": "언어",
|
|
73
74
|
"Unsupported language": "지원하지 않는 언어",
|
|
@@ -150,6 +151,7 @@ export const ko: Record<string, string> = {
|
|
|
150
151
|
"Toggle Status Bar": "상태 표시줄 전환",
|
|
151
152
|
"Toggle Value Flashing": "값 깜박임 전환",
|
|
152
153
|
"Check for Updates": "업데이트 확인",
|
|
154
|
+
"Release Channel": "릴리스 채널",
|
|
153
155
|
"Crash Reports": "충돌 보고",
|
|
154
156
|
"Usage Counts": "사용 횟수",
|
|
155
157
|
"Change Theme": "테마 변경",
|
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const zhCN: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "无法保存该值。",
|
|
71
71
|
"Could not apply that setting.": "无法应用该设置。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
|
|
73
74
|
"Published {date}": "已发布 {date}",
|
|
74
75
|
"Language": "语言",
|
|
75
76
|
"Unsupported language": "不支持的语言",
|
|
@@ -152,6 +153,7 @@ export const zhCN: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "切换状态栏",
|
|
153
154
|
"Toggle Value Flashing": "切换数值闪烁",
|
|
154
155
|
"Check for Updates": "检查更新",
|
|
156
|
+
"Release Channel": "发布通道",
|
|
155
157
|
"Crash Reports": "崩溃报告",
|
|
156
158
|
"Usage Counts": "使用统计",
|
|
157
159
|
"Change Theme": "更换主题",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const zhTW: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "無法儲存該值。",
|
|
71
71
|
"Could not apply that setting.": "無法套用該設定。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
|
|
73
74
|
"Published {date}": "已發布 {date}",
|
|
74
75
|
"Language": "語言",
|
|
75
76
|
"Unsupported language": "不支援的語言",
|
|
@@ -152,6 +153,7 @@ export const zhTW: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "切換狀態列",
|
|
153
154
|
"Toggle Value Flashing": "切換數值閃爍",
|
|
154
155
|
"Check for Updates": "檢查更新",
|
|
156
|
+
"Release Channel": "發布通道",
|
|
155
157
|
"Crash Reports": "當機報告",
|
|
156
158
|
"Usage Counts": "使用統計",
|
|
157
159
|
"Change Theme": "更換主題",
|
|
@@ -67,10 +67,14 @@ export class MarketDataCoordinatorEvents {
|
|
|
67
67
|
return (this.keyListeners.get(key)?.size ?? 0) > 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
getKeyVersion(key: string): number {
|
|
71
|
+
return this.keyVersions.get(key) ?? 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
getKeysVersion(keys: readonly string[]): number {
|
|
71
75
|
let version = 0;
|
|
72
76
|
for (const key of new Set(keys)) {
|
|
73
|
-
version += this.
|
|
77
|
+
version += this.getKeyVersion(key);
|
|
74
78
|
}
|
|
75
79
|
return version;
|
|
76
80
|
}
|
|
@@ -218,6 +218,10 @@ export class MarketDataCoordinator {
|
|
|
218
218
|
return this.events.getVersion();
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
getKeyVersion(key: string): number {
|
|
222
|
+
return this.events.getKeyVersion(key);
|
|
223
|
+
}
|
|
224
|
+
|
|
221
225
|
getKeysVersion(keys: readonly string[]): number {
|
|
222
226
|
return this.events.getKeysVersion(keys);
|
|
223
227
|
}
|
|
@@ -95,6 +95,10 @@ export async function loadQuoteEntry({
|
|
|
95
95
|
resolveQuote,
|
|
96
96
|
}: LoadQuoteEntryOptions): Promise<QueryEntry<Quote>> {
|
|
97
97
|
const key = buildQuoteKey(instrument);
|
|
98
|
+
if (!options.forceRefresh) {
|
|
99
|
+
const current = quoteStore.get(key);
|
|
100
|
+
if (hasFreshQuoteEntry(current, instrument, SNAPSHOT_CACHE_TTL_MS)) return current;
|
|
101
|
+
}
|
|
98
102
|
const flightKey = options.forceRefresh ? `${key}|refresh` : key;
|
|
99
103
|
return runSingleFlight(flightKey, async () => {
|
|
100
104
|
quoteStore.update(key, loadingEntry);
|