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
package/package.json
CHANGED
|
@@ -53,6 +53,86 @@ index c26c2c977ee1fc25a8dc144b85bac8626a45f572..8920a2905380228c23cba66c20d9bfd1
|
|
|
53
53
|
frame: {
|
|
54
54
|
width: this.frame.width,
|
|
55
55
|
height: this.frame.height,
|
|
56
|
+
diff --git a/dist/api/bun/core/Updater.ts b/dist/api/bun/core/Updater.ts
|
|
57
|
+
index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899c9464a5f 100644
|
|
58
|
+
--- a/dist/api/bun/core/Updater.ts
|
|
59
|
+
+++ b/dist/api/bun/core/Updater.ts
|
|
60
|
+
@@ -132,6 +132,19 @@ let updateInfo: {
|
|
61
|
+
error: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
+// Where updates are fetched from when it is not the channel and baseUrl that
|
|
65
|
+
+// version.json names. Only the download URLs read it: the app data folder and
|
|
66
|
+
+// the "dev" check keep the installed build's own channel.
|
|
67
|
+
+let updateSource: { channel: string; baseUrl: string } | null = null;
|
|
68
|
+
+
|
|
69
|
+
+function updateSourceChannel(): string {
|
|
70
|
+
+ return updateSource?.channel ?? localInfo.channel;
|
|
71
|
+
+}
|
|
72
|
+
+
|
|
73
|
+
+function updateSourceBaseUrl(): string {
|
|
74
|
+
+ return updateSource?.baseUrl ?? localInfo.baseUrl;
|
|
75
|
+
+}
|
|
76
|
+
+
|
|
77
|
+
function cleanupExtractionFolder(
|
|
78
|
+
extractionFolder: string,
|
|
79
|
+
keepTarHash: string,
|
|
80
|
+
@@ -176,6 +189,12 @@ const Updater = {
|
|
81
|
+
onStatusChangeCallback = callback;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
+ // Checks and downloads from `source` (a channel prefix and the URL its
|
|
85
|
+
+ // files sit under) until it is set again; null goes back to version.json.
|
|
86
|
+
+ setUpdateSource: (source: { channel: string; baseUrl: string } | null) => {
|
|
87
|
+
+ updateSource = source;
|
|
88
|
+
+ },
|
|
89
|
+
+
|
|
90
|
+
// todo: allow switching channels, by default will check the current channel
|
|
91
|
+
checkForUpdate: async () => {
|
|
92
|
+
emitStatus("checking", "Checking for updates...");
|
|
93
|
+
@@ -196,11 +215,11 @@ const Updater = {
|
|
94
|
+
|
|
95
|
+
const cacheBuster = Math.random().toString(36).substring(7);
|
|
96
|
+
const platformPrefix = getPlatformPrefix(
|
|
97
|
+
- localInfo.channel,
|
|
98
|
+
+ updateSourceChannel(),
|
|
99
|
+
currentOS,
|
|
100
|
+
currentArch,
|
|
101
|
+
);
|
|
102
|
+
- const updateInfoUrl = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-update.json?${cacheBuster}`;
|
|
103
|
+
+ const updateInfoUrl = `${updateSourceBaseUrl().replace(/\/+$/, "")}/${platformPrefix}-update.json?${cacheBuster}`;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const updateInfoResponse = await fetch(updateInfoUrl);
|
|
107
|
+
@@ -329,11 +348,11 @@ const Updater = {
|
|
108
|
+
|
|
109
|
+
// check if there's a patch file for it
|
|
110
|
+
const platformPrefix = getPlatformPrefix(
|
|
111
|
+
- localInfo.channel,
|
|
112
|
+
+ updateSourceChannel(),
|
|
113
|
+
currentOS,
|
|
114
|
+
currentArch,
|
|
115
|
+
);
|
|
116
|
+
- const patchUrl = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-${currentHash}.patch`;
|
|
117
|
+
+ const patchUrl = `${updateSourceBaseUrl().replace(/\/+$/, "")}/${platformPrefix}-${currentHash}.patch`;
|
|
118
|
+
emitStatus(
|
|
119
|
+
"fetching-patch",
|
|
120
|
+
`Checking for patch: ${currentHash.slice(0, 8)}`,
|
|
121
|
+
@@ -612,12 +631,12 @@ const Updater = {
|
|
122
|
+
|
|
123
|
+
const cacheBuster = Math.random().toString(36).substring(7);
|
|
124
|
+
const platformPrefix = getPlatformPrefix(
|
|
125
|
+
- localInfo.channel,
|
|
126
|
+
+ updateSourceChannel(),
|
|
127
|
+
currentOS,
|
|
128
|
+
currentArch,
|
|
129
|
+
);
|
|
130
|
+
const tarballName = getTarballFileName(appFileName, currentOS);
|
|
131
|
+
- const urlToLatestTarball = `${localInfo.baseUrl.replace(/\/+$/, "")}/${platformPrefix}-${tarballName}`;
|
|
132
|
+
+ const urlToLatestTarball = `${updateSourceBaseUrl().replace(/\/+$/, "")}/${platformPrefix}-${tarballName}`;
|
|
133
|
+
const prevVersionCompressedTarballPath = join(
|
|
134
|
+
appDataFolder,
|
|
135
|
+
"self-extraction",
|
|
56
136
|
diff --git a/dist/api/bun/proc/native.ts b/dist/api/bun/proc/native.ts
|
|
57
137
|
index f0f6c2532ba2eb467ec1d452a25c835b6db72661..ae77cf58d32392ad4cc8e2103ac7ed27baae8bea 100644
|
|
58
138
|
--- a/dist/api/bun/proc/native.ts
|
package/src/api-client/data.ts
CHANGED
|
@@ -953,8 +953,11 @@ export class CloudDataApi {
|
|
|
953
953
|
async getCloudSec13F(
|
|
954
954
|
path: string,
|
|
955
955
|
params: Record<string, string | number | undefined> = {},
|
|
956
|
+
options?: { signal?: AbortSignal },
|
|
956
957
|
): Promise<unknown> {
|
|
957
|
-
return this.request<unknown>(cloudSec13FPath(path, params)
|
|
958
|
+
return this.request<unknown>(cloudSec13FPath(path, params), {
|
|
959
|
+
signal: options?.signal,
|
|
960
|
+
});
|
|
958
961
|
}
|
|
959
962
|
|
|
960
963
|
/**
|
|
@@ -28,7 +28,10 @@ export type ResearchActivity =
|
|
|
28
28
|
| "onboarding_signed_in"
|
|
29
29
|
| "onboarding_pro_viewed"
|
|
30
30
|
| "onboarding_completed"
|
|
31
|
-
| "onboarding_skipped"
|
|
31
|
+
| "onboarding_skipped"
|
|
32
|
+
// The first action of a web visitor counted in the first-screen experiment,
|
|
33
|
+
// in that first session. Keep in sync with the server's list.
|
|
34
|
+
| "first_screen_engaged";
|
|
32
35
|
export type ResearchFeature =
|
|
33
36
|
| "overview"
|
|
34
37
|
| "chart"
|
|
@@ -451,7 +454,10 @@ function webExperimentAttribution(): { experiments?: string } {
|
|
|
451
454
|
* visitors on term.gloom.sh with an analytics id (no Do Not Track or Global
|
|
452
455
|
* Privacy Control) and not driven by automation are asked. See web-experiments.ts.
|
|
453
456
|
*/
|
|
454
|
-
export function exposeWebExperiment(
|
|
457
|
+
export function exposeWebExperiment(
|
|
458
|
+
experiment: string,
|
|
459
|
+
{ placement, signal }: { placement?: string; signal?: AbortSignal } = {},
|
|
460
|
+
): Promise<string | null> {
|
|
455
461
|
if (!researchTelemetryAllowed()) return Promise.resolve(null);
|
|
456
462
|
if (getCurrentPluginTarget() !== "web") return Promise.resolve(null);
|
|
457
463
|
try {
|
|
@@ -468,7 +474,8 @@ export function exposeWebExperiment(experiment: string): Promise<string | null>
|
|
|
468
474
|
attribution: attributionPayload(),
|
|
469
475
|
experiment: key,
|
|
470
476
|
variant,
|
|
471
|
-
|
|
477
|
+
...(placement && ACTIVITY_ID.test(placement) ? { placement } : {}),
|
|
478
|
+
}, signal),
|
|
472
479
|
});
|
|
473
480
|
} catch {
|
|
474
481
|
return Promise.resolve(null);
|
|
@@ -21,6 +21,7 @@ import type { DataProvider } from "../../types/data-provider";
|
|
|
21
21
|
import type { PinTickerOptions } from "../../types/plugin";
|
|
22
22
|
import {
|
|
23
23
|
resolveTickerOpenTarget,
|
|
24
|
+
tickerOpenFailureMessage,
|
|
24
25
|
type TickerOpenTarget,
|
|
25
26
|
} from "../../tickers/open-target";
|
|
26
27
|
import { AmbiguousTickerError, findExactTickerSearchMatch } from "../../tickers/search";
|
|
@@ -73,7 +74,7 @@ export function useAppTickerOpenRuntime({
|
|
|
73
74
|
tickerRepository,
|
|
74
75
|
});
|
|
75
76
|
if (!target && canPresentFeedback()) {
|
|
76
|
-
pluginRegistry.notify({ body:
|
|
77
|
+
pluginRegistry.notify({ body: tickerOpenFailureMessage(rawSymbol), type: "error" });
|
|
77
78
|
}
|
|
78
79
|
return target;
|
|
79
80
|
} catch (err) {
|
|
@@ -7,13 +7,17 @@ import {
|
|
|
7
7
|
canRestartToApply,
|
|
8
8
|
canSelfUpdate,
|
|
9
9
|
checkForUpdateDetailed,
|
|
10
|
+
discardStagedUpdate,
|
|
10
11
|
performUpdate,
|
|
11
12
|
type ReleaseInfo,
|
|
12
13
|
type UpdateCheckResult,
|
|
13
14
|
type UpdateProgress,
|
|
14
15
|
} from "../../updater";
|
|
16
|
+
import { resolveUpdateChannel, stagedUpdateFitsChannel } from "../../updater/channel";
|
|
17
|
+
import type { UpdateChannel } from "../../types/config";
|
|
15
18
|
import { VERSION } from "../../version";
|
|
16
19
|
import { debugLog } from "../../utils/debug-log";
|
|
20
|
+
import { isPrereleaseVersion } from "../../utils/semver";
|
|
17
21
|
import { reportCrash } from "../../telemetry/crash-reports";
|
|
18
22
|
import { runAutomated } from "../../telemetry/usage-counts";
|
|
19
23
|
|
|
@@ -41,6 +45,7 @@ export function useAppUpdateRuntime({
|
|
|
41
45
|
pluginRegistry,
|
|
42
46
|
stateRef,
|
|
43
47
|
updateAvailable,
|
|
48
|
+
updateChannel = "stable",
|
|
44
49
|
updateCheckInProgress,
|
|
45
50
|
updateProgress,
|
|
46
51
|
}: {
|
|
@@ -50,6 +55,8 @@ export function useAppUpdateRuntime({
|
|
|
50
55
|
pluginRegistry: PluginRegistry;
|
|
51
56
|
stateRef: { current: AppState };
|
|
52
57
|
updateAvailable: ReleaseInfo | null;
|
|
58
|
+
/** The channel in effect (`resolveUpdateChannel`); a change checks again at once. */
|
|
59
|
+
updateChannel?: UpdateChannel;
|
|
53
60
|
updateCheckInProgress: boolean;
|
|
54
61
|
updateProgress: unknown;
|
|
55
62
|
}): {
|
|
@@ -97,7 +104,8 @@ export function useAppUpdateRuntime({
|
|
|
97
104
|
// failed check like any other: the hourly one must not end as an
|
|
98
105
|
// unhandled rejection, and a manual one must clear its progress state.
|
|
99
106
|
// The timeout is still reported, as it was when it went unhandled.
|
|
100
|
-
const
|
|
107
|
+
const channel = resolveUpdateChannel(stateRef.current.config);
|
|
108
|
+
const result = await checkForUpdateDetailed(VERSION, channel).catch((error: unknown): UpdateCheckResult => {
|
|
101
109
|
const message = error instanceof Error ? error.message : "";
|
|
102
110
|
if (message.startsWith("RPC request timed out")) {
|
|
103
111
|
reportCrash(error, { kind: "unhandled-rejection" });
|
|
@@ -123,7 +131,7 @@ export function useAppUpdateRuntime({
|
|
|
123
131
|
|
|
124
132
|
if (result.kind === "current") {
|
|
125
133
|
dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
|
|
126
|
-
dispatch({ type: "SET_UPDATE_NOTICE", notice: `Already on v${VERSION}` });
|
|
134
|
+
dispatch({ type: "SET_UPDATE_NOTICE", notice: result.notice ?? `Already on v${VERSION}` });
|
|
127
135
|
return;
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -147,6 +155,44 @@ export function useAppUpdateRuntime({
|
|
|
147
155
|
return () => { clearInterval(interval); };
|
|
148
156
|
}, [enabled, isDetachedWindow, runUpdateCheck]);
|
|
149
157
|
|
|
158
|
+
// A channel switch checks again at once and says what it found: nightly
|
|
159
|
+
// starts on today's build, stable says when this nightly build stays. An
|
|
160
|
+
// update already downloading or waiting for a restart is left to the effect
|
|
161
|
+
// below, which takes it back once it is ready if the channel rules it out.
|
|
162
|
+
const checkedChannelRef = useRef(updateChannel);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!enabled || isDetachedWindow || checkedChannelRef.current === updateChannel) return;
|
|
165
|
+
checkedChannelRef.current = updateChannel;
|
|
166
|
+
if (updateInFlight(stateRef.current.updateProgress)) return;
|
|
167
|
+
// What the other channel offered, or how its download failed, is not this one's answer.
|
|
168
|
+
if (stateRef.current.updateProgress) dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
|
|
169
|
+
dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
|
|
170
|
+
void runUpdateCheck(true);
|
|
171
|
+
}, [dispatch, enabled, isDetachedWindow, runUpdateCheck, stateRef, updateChannel]);
|
|
172
|
+
|
|
173
|
+
// A downloaded update the channel does not allow (a nightly build waiting
|
|
174
|
+
// for a restart after the switch back to stable, or one that finished
|
|
175
|
+
// downloading after it) is taken back rather than installed by the restart,
|
|
176
|
+
// and stable checks again.
|
|
177
|
+
const discardingRef = useRef(false);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!enabled || isDetachedWindow || discardingRef.current) return;
|
|
180
|
+
const { updateProgress: progress, updateAvailable: staged } = stateRef.current;
|
|
181
|
+
if (progress?.phase !== "ready" || stagedUpdateFitsChannel(staged, updateChannel)) return;
|
|
182
|
+
discardingRef.current = true;
|
|
183
|
+
void discardStagedUpdate().then((error) => {
|
|
184
|
+
discardingRef.current = false;
|
|
185
|
+
if (error) {
|
|
186
|
+
updateLog.warn("could not take back the staged update", { error, version: staged?.version });
|
|
187
|
+
dispatch({ type: "SET_UPDATE_NOTICE", notice: `Could not take back v${staged?.version ?? "?"}: ${error}` });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
|
|
191
|
+
dispatch({ type: "SET_UPDATE_AVAILABLE", release: null });
|
|
192
|
+
void runUpdateCheck(true);
|
|
193
|
+
});
|
|
194
|
+
}, [dispatch, enabled, isDetachedWindow, runUpdateCheck, stateRef, updateAvailable, updateChannel, updateProgress]);
|
|
195
|
+
|
|
150
196
|
// First launch on a new version: show that release's notes, then remember the version.
|
|
151
197
|
useEffect(() => {
|
|
152
198
|
if (!enabled || isDetachedWindow) return;
|
|
@@ -156,7 +202,8 @@ export function useAppUpdateRuntime({
|
|
|
156
202
|
const nextConfig = { ...config, lastLaunchedVersion: VERSION };
|
|
157
203
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
158
204
|
void saveConfigImmediately(nextConfig).catch(() => {});
|
|
159
|
-
|
|
205
|
+
// Nightly builds come daily and the changelog lists stable releases only.
|
|
206
|
+
if (!isUpgrade || isPrereleaseVersion(VERSION)) return;
|
|
160
207
|
// Opened by the app, not the user: usage counts leave it out.
|
|
161
208
|
void runAutomated(() => pluginRegistry.createPaneFromTemplateAsync("changelog-pane", {
|
|
162
209
|
values: { version: VERSION },
|
package/src/app.tsx
CHANGED
|
@@ -50,6 +50,7 @@ import { bindPluginRegistryRuntimeAccess } from "./app/runtime/plugin-bindings";
|
|
|
50
50
|
import { useAppStartupRuntime } from "./app/runtime/startup";
|
|
51
51
|
import { useTickerRefreshRuntime } from "./app/runtime/ticker-refresh";
|
|
52
52
|
import { useAppUpdateRuntime } from "./app/runtime/update";
|
|
53
|
+
import { resolveUpdateChannel } from "./updater/channel";
|
|
53
54
|
import { createCoreSyncContributors } from "./sync/core-contributors";
|
|
54
55
|
import { useCloudSyncRuntime } from "./sync/react";
|
|
55
56
|
import { RemoteControlHost, type RemoteControlAdapter } from "./remote/app-host";
|
|
@@ -278,6 +279,7 @@ function AppInner({
|
|
|
278
279
|
pluginRegistry,
|
|
279
280
|
stateRef,
|
|
280
281
|
updateAvailable: state.updateAvailable,
|
|
282
|
+
updateChannel: resolveUpdateChannel(state.config),
|
|
281
283
|
updateCheckInProgress: state.updateCheckInProgress,
|
|
282
284
|
updateProgress: state.updateProgress,
|
|
283
285
|
});
|
|
@@ -41,7 +41,7 @@ function scalarArgs(source: ArgSource, privateFields: PaneSharePrivateFields | u
|
|
|
41
41
|
* that opens this pane (a chart opened with `GP` stays `GP`), else the first
|
|
42
42
|
* template that opens it.
|
|
43
43
|
*/
|
|
44
|
-
function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType: string, title: string): string | null {
|
|
44
|
+
export function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType: string, title: string): string | null {
|
|
45
45
|
const prefixes = [...registry.paneTemplates.values()]
|
|
46
46
|
.filter((template) => template.paneId === paneType && template.shortcut?.prefix)
|
|
47
47
|
.map((template) => template.shortcut!.prefix.toUpperCase());
|
|
@@ -50,6 +50,16 @@ function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType:
|
|
|
50
50
|
return prefixes[0] ?? null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A pane named by its function and what it shows, such as `DES AAPL`: its
|
|
55
|
+
* title, led by its function code unless the title already starts with it.
|
|
56
|
+
*/
|
|
57
|
+
export function paneFunctionLabel(pane: { title: string; code: string | null; paneType: string }): string {
|
|
58
|
+
const title = pane.title.trim() || pane.paneType;
|
|
59
|
+
if (!pane.code) return title;
|
|
60
|
+
return title.split(/\s+/)[0]?.toUpperCase() === pane.code ? title : `${pane.code} ${title}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export interface PaneAttachmentSource {
|
|
54
64
|
registry: Pick<PluginRegistry, "paneTemplates" | "brokers" | "getPanePluginId" | "getBrokerPluginId">;
|
|
55
65
|
pane: Pick<ResolvedPane, "instance" | "def">;
|
|
@@ -428,7 +428,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
428
428
|
}),
|
|
429
429
|
}, {
|
|
430
430
|
dateKey: "date",
|
|
431
|
-
empty: fredEmptyMessage(startDate, latest),
|
|
431
|
+
empty: fredEmptyMessage(startDate, latest == null ? null : exportRow(latest)),
|
|
432
432
|
...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
|
|
433
433
|
exportIdentity: { symbolOrSeries: seriesId, currencyOrUnit: unit, interval },
|
|
434
434
|
textColumns: [
|
|
@@ -486,7 +486,7 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
486
486
|
key: "changeBasisPoints",
|
|
487
487
|
header: "1D bp",
|
|
488
488
|
align: "right",
|
|
489
|
-
format: (value) => typeof value === "number" ? formatBasisPoints(value / 100) : "-",
|
|
489
|
+
format: (value) => typeof value === "number" ? formatBasisPoints(value / 100, 1) : "-",
|
|
490
490
|
},
|
|
491
491
|
{ key: "date", header: "Date" },
|
|
492
492
|
{ key: "seriesId", header: "Series" },
|
|
@@ -4,6 +4,7 @@ import { saveConfig } from "../../data/config/store";
|
|
|
4
4
|
import { builtinPluginGroupMembers } from "../../plugins/ownership";
|
|
5
5
|
import type { AppConfig, TelemetryConfig } from "../../types/config";
|
|
6
6
|
import { canonicalTimeZone } from "../../utils/utc-time";
|
|
7
|
+
import { NIGHTLY_CHANNEL_SUMMARY, parseUpdateChannel, withUpdateChannel } from "../../updater/channel";
|
|
7
8
|
import type { CliCommandDef } from "../../types/plugin";
|
|
8
9
|
import { resolveCliDataDir, withCliServices, withConfigData } from "../context";
|
|
9
10
|
import { CLI_COMMAND_GROUPS } from "../help";
|
|
@@ -33,6 +34,8 @@ const STAR_PROMPT_CONFIG_KEY = "starPrompt.enabled";
|
|
|
33
34
|
const TIMEZONE_CONFIG_KEY = "timezone";
|
|
34
35
|
/** What `config set timezone` takes to go back to UTC alone. */
|
|
35
36
|
const NO_TIMEZONE = "none";
|
|
37
|
+
/** Stable releases, or nightly builds as well. */
|
|
38
|
+
const UPDATE_CHANNEL_CONFIG_KEY = "updates.channel";
|
|
36
39
|
const EDITABLE_CONFIG_KEYS = [
|
|
37
40
|
"baseCurrency",
|
|
38
41
|
"refreshIntervalMinutes",
|
|
@@ -41,6 +44,7 @@ const EDITABLE_CONFIG_KEYS = [
|
|
|
41
44
|
...Object.keys(TELEMETRY_CONFIG_KEYS),
|
|
42
45
|
STAR_PROMPT_CONFIG_KEY,
|
|
43
46
|
TIMEZONE_CONFIG_KEY,
|
|
47
|
+
UPDATE_CHANNEL_CONFIG_KEY,
|
|
44
48
|
];
|
|
45
49
|
|
|
46
50
|
function isTelemetryConfigKey(key: string): key is TelemetryConfigKey {
|
|
@@ -219,6 +223,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
219
223
|
"config set telemetry.usage false",
|
|
220
224
|
"config set starPrompt.enabled false",
|
|
221
225
|
"config set timezone <Region/City>|none",
|
|
226
|
+
"config set updates.channel stable|nightly",
|
|
222
227
|
"config get keybindings",
|
|
223
228
|
"config set keybindings.actions.<action> <keys>|null|default",
|
|
224
229
|
"config set keybindings.commands.<keys> <command>|null",
|
|
@@ -236,6 +241,11 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
236
241
|
lines: [
|
|
237
242
|
`theme takes one of these ids, or its name as config themes lists it: ${getThemeIds().join(", ")}.`,
|
|
238
243
|
],
|
|
244
|
+
}, {
|
|
245
|
+
title: "Release channel",
|
|
246
|
+
lines: [
|
|
247
|
+
`The app updates itself from stable releases. With updates.channel nightly (Release Channel in the command bar) it also installs the nightly build: ${NIGHTLY_CHANNEL_SUMMARY}. Back on stable, a nightly build stays until a newer stable release is out. GLOOMBERB_UPDATE_CHANNEL overrides the setting for one run; npm and Bun installs always follow stable.`,
|
|
248
|
+
],
|
|
239
249
|
}],
|
|
240
250
|
examples: [
|
|
241
251
|
"config",
|
|
@@ -287,6 +297,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
287
297
|
"telemetry.attention": context.config.telemetry?.attention === true,
|
|
288
298
|
[STAR_PROMPT_CONFIG_KEY]: context.config.starPrompt?.enabled !== false,
|
|
289
299
|
[TIMEZONE_CONFIG_KEY]: context.config.timezone ?? null,
|
|
300
|
+
[UPDATE_CHANNEL_CONFIG_KEY]: context.config.updates?.channel ?? "stable",
|
|
290
301
|
};
|
|
291
302
|
|
|
292
303
|
if (action === "list") {
|
|
@@ -347,11 +358,16 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
347
358
|
if ((isTelemetryConfigKey(key) || key === STAR_PROMPT_CONFIG_KEY) && value !== "true" && value !== "false") {
|
|
348
359
|
ctx.fail(`Usage: gloomberb config set ${key} true|false`);
|
|
349
360
|
}
|
|
361
|
+
if (key === UPDATE_CHANNEL_CONFIG_KEY && !parseUpdateChannel(value)) {
|
|
362
|
+
ctx.fail(`Usage: gloomberb config set ${key} stable|nightly`);
|
|
363
|
+
}
|
|
350
364
|
// A display name may come unquoted: config set theme White Phosphor.
|
|
351
365
|
const parsedValue = key === "theme"
|
|
352
366
|
? requireThemeId(args.slice(2).join(" "))
|
|
353
367
|
: key === TIMEZONE_CONFIG_KEY
|
|
354
368
|
? parseTimezoneSetting(value)
|
|
369
|
+
: key === UPDATE_CHANNEL_CONFIG_KEY
|
|
370
|
+
? parseUpdateChannel(value)
|
|
355
371
|
: key === "refreshIntervalMinutes"
|
|
356
372
|
? Number(value)
|
|
357
373
|
: isBooleanConfigKey(key)
|
|
@@ -363,7 +379,9 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
363
379
|
? { ...context.config, starPrompt: { ...context.config.starPrompt, enabled: parsedValue as boolean } }
|
|
364
380
|
: key === TIMEZONE_CONFIG_KEY
|
|
365
381
|
? withTimezone(context.config, parsedValue as string | null)
|
|
366
|
-
:
|
|
382
|
+
: key === UPDATE_CHANNEL_CONFIG_KEY
|
|
383
|
+
? withUpdateChannel(context.config, parsedValue as "stable" | "nightly")
|
|
384
|
+
: { ...context.config, [key]: parsedValue };
|
|
367
385
|
if (!ctx.cliOptions.dryRun) await saveConfig(nextConfig);
|
|
368
386
|
ctx.printResult({
|
|
369
387
|
data: {
|
package/src/cli/glossary.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { cliStyles, cliTerminalWidth, renderDefinitions, renderSection, wrapText
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Plain-words meanings of the rates, auction, energy, perpetual-futures, portfolio,
|
|
5
|
-
* fund, short interest, quote-board, futures and economic-calendar terms the functions
|
|
5
|
+
* fund, short interest, quote-board, futures (basis, contango, roll yield) and economic-calendar terms the functions
|
|
6
6
|
* show in their rows, headers and footers.
|
|
7
7
|
* `catalog glossary` lists them, `catalog explain <term>` prints one, and
|
|
8
8
|
* `fn <function> --explain` and `portfolio show --explain` follow a report with
|
|
@@ -301,10 +301,11 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
301
301
|
+ "The funding rate keeps its price close to the asset it tracks.",
|
|
302
302
|
},
|
|
303
303
|
{
|
|
304
|
-
term: "Funding rate", aliases: ["funding", "funding payment", "funding fee", "funding 8h"], functions: ["PERP"],
|
|
305
|
-
shown: ["Funding %/int", "Funding 8h", "Paid %/int"],
|
|
304
|
+
term: "Funding rate", aliases: ["funding", "funding payment", "funding fee", "funding 8h", "funding apr", "apr"], functions: ["PERP"],
|
|
305
|
+
shown: ["Funding %/int", "Funding 8h", "Paid %/int", "APR %"],
|
|
306
306
|
definition: "A regular payment between the two sides of a perpetual contract, set to pull its price toward the asset's. "
|
|
307
|
-
+ "Positive means longs pay shorts, negative means shorts pay longs."
|
|
307
|
+
+ "Positive means longs pay shorts, negative means shorts pay longs, so a basis trade (long spot, short the perpetual) collects it when positive. "
|
|
308
|
+
+ "APR % is the current 8h rate × 3 × 365: a simple annualisation of the rate now, not funding actually paid.",
|
|
308
309
|
},
|
|
309
310
|
{
|
|
310
311
|
term: "Open interest", short: "OI", aliases: ["open interest usd"], functions: ["PERP"],
|
|
@@ -473,6 +474,35 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
473
474
|
definition: "A futures contract's official price for the day, set by its exchange from trading near the close. "
|
|
474
475
|
+ "The day's change and margin are measured from it, and it can differ from the last trade. FUT's PREV is the previous one.",
|
|
475
476
|
},
|
|
477
|
+
// Futures basis and curve shape (CTM, FUT, PERP).
|
|
478
|
+
{
|
|
479
|
+
term: "Futures basis", aliases: ["basis", "basis trade", "cash and carry", "cash-and-carry", "vs spot", "spot spread"],
|
|
480
|
+
functions: ["CTM", "FUT", "PERP"], shown: ["Vs Spot", "VS SPOT"],
|
|
481
|
+
definition: "The gap between a futures price and the spot price of what it tracks, in percent: positive when the future costs more, negative when it costs less. "
|
|
482
|
+
+ "It closes by expiry. A basis trade buys spot and sells the future to earn it. A perpetual has no expiry, so its funding rate plays that part.",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
term: "Annualised basis", aliases: ["annualized basis", "ann basis", "ann. basis", "basis annualised"], functions: ["CTM", "PERP"], shown: ["Ann Basis", "ANN BASIS"],
|
|
486
|
+
definition: "The basis restated as a yearly rate, so contracts of different length compare with each other and with a perpetual's funding APR: "
|
|
487
|
+
+ "(Last / spot - 1) × 365 / days to expiry, simple rather than compounded, on the latest trade against the spot quote. "
|
|
488
|
+
+ "Inside 30 days a few basis points of spot noise move it by whole points: 10 bp is about ±2 points at 18 days to expiry.",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
term: "Contango", aliases: ["contango curve", "upward curve"], functions: ["CTM", "FUT"], shown: ["contango", "Contango"],
|
|
492
|
+
definition: "A futures curve that slopes up: each contract costs more than the one before it, and the future trades above spot. "
|
|
493
|
+
+ "A long rolling into the next contract pays that difference, a short earns it, and the basis closes as expiry nears.",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
term: "Backwardation", aliases: ["backwardation curve", "downward curve", "inverted futures curve"], functions: ["CTM", "FUT"], shown: ["backwardation", "Backwardation"],
|
|
497
|
+
definition: "A futures curve that slopes down: later contracts cost less than nearer ones, and the future trades below spot, often when the asset is scarce now. "
|
|
498
|
+
+ "A long rolling into the next contract earns that difference, a short pays it.",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
term: "Roll yield", aliases: ["roll", "roll return", "annualised roll yield", "annualized roll yield", "ann roll yield", "ann. roll yield"], functions: ["CTM", "FUT"],
|
|
502
|
+
shown: ["Ann. roll yield", "Annualized Roll Yield"],
|
|
503
|
+
definition: "What a long future earns or loses as it is rolled into the next contract. CTM's annualised roll yield is the front price over the next contract's "
|
|
504
|
+
+ "less 1, scaled to a year: (front / next - 1) × 365 / days between the two. It is positive in backwardation and negative in contango.",
|
|
505
|
+
},
|
|
476
506
|
// The economic calendar (ECO).
|
|
477
507
|
{
|
|
478
508
|
term: "Actual", aliases: ["actual release", "actual value"], functions: ["ECO"], shown: ["ACTUAL", "Actual"],
|
package/src/cli/index.ts
CHANGED
|
@@ -251,6 +251,12 @@ function createCoreCliCommands(
|
|
|
251
251
|
help: {
|
|
252
252
|
group: CLI_COMMAND_GROUPS.plugins,
|
|
253
253
|
usage: ["update [name]"],
|
|
254
|
+
sections: [{
|
|
255
|
+
title: "Updating Gloomberb itself",
|
|
256
|
+
lines: [
|
|
257
|
+
"The app updates itself from stable releases. config set updates.channel nightly adds the nightly build of main; see gloomberb help config.",
|
|
258
|
+
],
|
|
259
|
+
}],
|
|
254
260
|
examples: ["update", "update gloom-tv"],
|
|
255
261
|
},
|
|
256
262
|
execute: async (args) => {
|
|
@@ -756,7 +756,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
756
756
|
bloomberg: ["GLCO"],
|
|
757
757
|
},
|
|
758
758
|
CTM: {
|
|
759
|
-
summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote.",
|
|
759
|
+
summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote: Ann Basis = (Last / spot - 1) × 365 / days to expiry, simple actual/365.",
|
|
760
760
|
usage: ["CTM GC", "CTM BTC"],
|
|
761
761
|
keys: [key("d", "ate"), key("c", "urrent"), STEP],
|
|
762
762
|
data: same("Delayed, usually 10 minutes; VIX at settlement"),
|
|
@@ -48,7 +48,8 @@ const US_EXCHANGE_TIME_ZONE = "America/New_York";
|
|
|
48
48
|
/**
|
|
49
49
|
* A venue's session now, from its calendar (weekends and published holidays),
|
|
50
50
|
* or from the state its source reports when the app has no calendar for it,
|
|
51
|
-
* such as an index publisher or a futures exchange
|
|
51
|
+
* such as an index publisher or a futures exchange; an index quote that states
|
|
52
|
+
* none is placed on the sessions it carries. Null when none of them says.
|
|
52
53
|
*/
|
|
53
54
|
function venueSession(exchange: string, marketState: string | undefined, now: number, calendar?: QuoteIndexSession): VenueSession | null {
|
|
54
55
|
if (isAlwaysOpenExchange(exchange)) return { state: "always" };
|
|
@@ -68,8 +69,22 @@ function venueSession(exchange: string, marketState: string | undefined, now: nu
|
|
|
68
69
|
}
|
|
69
70
|
if (marketState === "REGULAR") return { state: "open" };
|
|
70
71
|
const stated = marketState === "PRE" ? "pre-market" : marketState === "POST" ? "after-hours" : marketState && CLOSED_STATES.has(marketState) ? "closed" : null;
|
|
71
|
-
if (!
|
|
72
|
-
|
|
72
|
+
if (!calendar) return stated ? { state: stated } : null;
|
|
73
|
+
const state = stated ?? quotedState(calendar, now);
|
|
74
|
+
return state ? { state, ...quotedSessions(calendar, now) } : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where an index quote's own sessions put the market now, for a quote that
|
|
79
|
+
* states no session. Before the open it is pre-market on the day of the open,
|
|
80
|
+
* as the venue calendars have it, and closed on any earlier day (a weekend, a holiday).
|
|
81
|
+
*/
|
|
82
|
+
function quotedState({ timeZone, current, regular }: QuoteIndexSession, now: number): ReportMarketState | null {
|
|
83
|
+
if (now >= current.end) return "closed";
|
|
84
|
+
if (now >= current.start) return "open";
|
|
85
|
+
// A regular session still running is not today's: the sessions disagree, so say nothing.
|
|
86
|
+
if (regular.some((window) => window.end > now)) return null;
|
|
87
|
+
return zonedDateKey(now, timeZone) === zonedDateKey(current.start, timeZone) ? "pre-market" : "closed";
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
@@ -500,6 +500,13 @@ export function formatCompositeCursorAxisDate(
|
|
|
500
500
|
|
|
501
501
|
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
502
502
|
|
|
503
|
+
/** A dataset observation's day as the axis words it, `Oct 4`, with the year when the chart ends in another. */
|
|
504
|
+
export function formatCompositeDatasetDate(date: Date, endTime: number | undefined): string {
|
|
505
|
+
const text = `${MONTH_NAMES[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
506
|
+
const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
|
|
507
|
+
return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
503
510
|
/**
|
|
504
511
|
* When a value carried under the cursor was observed: its time when it is
|
|
505
512
|
* from the cursor's own day, else its date (`Sep 2`, with the year when that
|
|
@@ -5,7 +5,13 @@ import { CANONICAL_EXCHANGE_ALIASES } from "../../../utils/exchanges";
|
|
|
5
5
|
import { displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
6
6
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
7
7
|
import { consumeChartMouseEvent, type ChartMouseEvent } from "../core/pointer";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatCompositeCarriedStamp,
|
|
10
|
+
formatCompositeDatasetDate,
|
|
11
|
+
formatCompositePointDetails,
|
|
12
|
+
formatCompositeSeriesValue,
|
|
13
|
+
seriesPriceReference,
|
|
14
|
+
} from "./format";
|
|
9
15
|
import type { CompositeChartProps, CompositeChartScene } from "./types";
|
|
10
16
|
import { seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
|
|
11
17
|
|
|
@@ -45,15 +51,6 @@ function axisPriceReference(scene: CompositeChartScene | null, series: ResolvedS
|
|
|
45
51
|
return own;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
49
|
-
|
|
50
|
-
/** `Oct 4`, the axis's own wording, with the year when the chart ends in another. */
|
|
51
|
-
function legendDateText(date: Date, endTime: number | undefined): string {
|
|
52
|
-
const text = `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
53
|
-
const endYear = endTime === undefined ? null : new Date(endTime).getUTCFullYear();
|
|
54
|
-
return endYear === null || endYear === date.getUTCFullYear() ? text : `${text} ${date.getUTCFullYear()}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
/**
|
|
58
55
|
* A dataset's values arrive days late, so the legend dates them. Idle, one
|
|
59
56
|
* date leads the legend when it is every value's; otherwise, and under the
|
|
@@ -72,7 +69,7 @@ function legendValueDates(
|
|
|
72
69
|
valued += 1;
|
|
73
70
|
if (entry.observationKind === "dataset" && cursorValue.point) dates.set(entry.id, cursorValue.point.date);
|
|
74
71
|
}
|
|
75
|
-
const text = new Map([...dates].map(([id, date]) => [id,
|
|
72
|
+
const text = new Map([...dates].map(([id, date]) => [id, formatCompositeDatasetDate(date, scene?.endTime)] as const));
|
|
76
73
|
const cursorDate = scene?.cursorDate;
|
|
77
74
|
if (cursorDate) {
|
|
78
75
|
const cursorDay = cursorDate.toISOString().slice(0, 10);
|
|
@@ -29,6 +29,9 @@ import { parseWindowModeCommandArg } from "../../layout-items";
|
|
|
29
29
|
import type { CommandBarRoute } from "../../workflow/types";
|
|
30
30
|
import { recordFunctionOpen } from "../../../../telemetry/usage-counts";
|
|
31
31
|
import { reportTelemetryConfig } from "../../../../telemetry/live-config";
|
|
32
|
+
import { NIGHTLY_CHANNEL_SUMMARY, withUpdateChannel } from "../../../../updater/channel";
|
|
33
|
+
import { isPrereleaseVersion } from "../../../../utils/semver";
|
|
34
|
+
import { VERSION } from "../../../../version";
|
|
32
35
|
|
|
33
36
|
type NotifyFn = (body: string, options?: { type?: "info" | "success" | "error" }) => void;
|
|
34
37
|
|
|
@@ -373,6 +376,50 @@ export function runDirectCommandAction(options: {
|
|
|
373
376
|
void onCheckForUpdates?.();
|
|
374
377
|
closeAll({ revertThemePreview: false });
|
|
375
378
|
return;
|
|
379
|
+
case "release-channel": {
|
|
380
|
+
// The runtime checks again as soon as the channel changes.
|
|
381
|
+
const setChannel = (channel: "stable" | "nightly") => {
|
|
382
|
+
const nextConfig = withUpdateChannel(getState().config, channel);
|
|
383
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
384
|
+
persistConfig(nextConfig);
|
|
385
|
+
notify(`${t(command.label)}: ${channel}`, { type: "success" });
|
|
386
|
+
};
|
|
387
|
+
// The saved setting flips; GLOOMBERB_UPDATE_CHANNEL still wins for the run it is set on.
|
|
388
|
+
// Back to stable asks too, to say a nightly build already running stays.
|
|
389
|
+
if (state.config.updates?.channel === "nightly") {
|
|
390
|
+
openInlineConfirm({
|
|
391
|
+
confirmId: "switch-to-stable",
|
|
392
|
+
title: "Switch to Stable?",
|
|
393
|
+
body: [
|
|
394
|
+
isPrereleaseVersion(VERSION)
|
|
395
|
+
? "Updates come from stable releases only. This nightly build stays until a newer stable release is out."
|
|
396
|
+
: "Updates come from stable releases only.",
|
|
397
|
+
],
|
|
398
|
+
confirmLabel: "Switch to Stable",
|
|
399
|
+
cancelLabel: "Stay on Nightly",
|
|
400
|
+
tone: "default",
|
|
401
|
+
onConfirm: async () => { setChannel("stable"); },
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
openInlineConfirm({
|
|
406
|
+
confirmId: "switch-to-nightly",
|
|
407
|
+
title: "Switch to Nightly?",
|
|
408
|
+
body: [
|
|
409
|
+
`Nightly is ${NIGHTLY_CHANNEL_SUMMARY}. Stable releases still reach you when they are newer.`,
|
|
410
|
+
isPrereleaseVersion(VERSION)
|
|
411
|
+
? "Switch back to Stable at any time."
|
|
412
|
+
: "Switch back to Stable at any time: the nightly build stays until a newer stable release is out.",
|
|
413
|
+
],
|
|
414
|
+
confirmLabel: "Switch to Nightly",
|
|
415
|
+
cancelLabel: "Stay on Stable",
|
|
416
|
+
tone: "default",
|
|
417
|
+
// An opt-in to builds that can break: a stray Enter stays on stable.
|
|
418
|
+
defaultAction: "cancel",
|
|
419
|
+
onConfirm: async () => { setChannel("nightly"); },
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
376
423
|
case "theme":
|
|
377
424
|
setRootThemeBaseId(getState().config.theme);
|
|
378
425
|
setRootQuery(arg ? `TH ${arg}` : "TH ");
|