gloomberb 0.10.2 → 0.10.4
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 +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/components/ui/data-table/index.tsx +1 -0
- package/src/components/ui/data-table/opentui/index.tsx +44 -2
- package/src/components/ui/data-table/types.ts +10 -0
- package/src/components/ui/data-table/visible-range.test.ts +29 -0
- package/src/components/ui/data-table/visible-range.ts +21 -0
- package/src/components/ui/ui.test.tsx +23 -1
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +66 -3
- package/src/i18n/ko.ts +66 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +66 -3
- package/src/i18n/zh-tw.ts +66 -3
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/coordinator/quotes.ts +64 -1
- package/src/market-data/coordinator/subscriptions.test.ts +72 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -112
- package/src/plugins/builtin/options/footer.ts +35 -78
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
- package/src/plugins/builtin/options/live-quotes.ts +93 -39
- package/src/plugins/builtin/options/view.test.tsx +5 -2
- package/src/plugins/builtin/options/view.tsx +49 -9
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +181 -1
- package/src/state/hooks/quote-streaming.ts +93 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +17 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, jest, test } from "bun:test";
|
|
2
2
|
import { MarketDataCoordinator } from "./index";
|
|
3
3
|
import { buildQuoteKey } from "../selectors";
|
|
4
|
+
import { QUOTE_STREAM_UPDATE_THROTTLE_MS } from "../quotes/cadence";
|
|
4
5
|
import { createTestDataProvider } from "../../test-support/data-provider";
|
|
5
6
|
import type { Quote } from "../../types/financials";
|
|
6
7
|
import type { DataProvider, QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
@@ -208,6 +209,38 @@ describe("MarketDataCoordinator key subscriptions", () => {
|
|
|
208
209
|
expect(coordinator.getVersion()).toBe(1);
|
|
209
210
|
});
|
|
210
211
|
|
|
212
|
+
test("coalesces bursty stream quotes into one readable update cadence", () => {
|
|
213
|
+
jest.useFakeTimers();
|
|
214
|
+
const originalDateNow = Date.now;
|
|
215
|
+
let now = 1_700_000_000_000;
|
|
216
|
+
Date.now = () => now;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const { provider, emitQuote } = createProvider();
|
|
220
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
221
|
+
const amd = { symbol: "AMD", exchange: "NASDAQ" };
|
|
222
|
+
const msft = { symbol: "MSFT", exchange: "NASDAQ" };
|
|
223
|
+
coordinator.subscribeQuotes([{ instrument: amd }, { instrument: msft }]);
|
|
224
|
+
|
|
225
|
+
emitQuote(amd, quote("AMD", 100));
|
|
226
|
+
expect(coordinator.getQuoteEntry(amd).data?.price).toBe(100);
|
|
227
|
+
|
|
228
|
+
now += 100;
|
|
229
|
+
emitQuote(msft, quote("MSFT", 200));
|
|
230
|
+
emitQuote(amd, quote("AMD", 101));
|
|
231
|
+
expect(coordinator.getQuoteEntry(amd).data?.price).toBe(100);
|
|
232
|
+
expect(coordinator.getQuoteEntry(msft).data).toBeNull();
|
|
233
|
+
|
|
234
|
+
now += QUOTE_STREAM_UPDATE_THROTTLE_MS - 100;
|
|
235
|
+
jest.advanceTimersByTime(QUOTE_STREAM_UPDATE_THROTTLE_MS - 100);
|
|
236
|
+
expect(coordinator.getQuoteEntry(amd).data?.price).toBe(101);
|
|
237
|
+
expect(coordinator.getQuoteEntry(msft).data?.price).toBe(200);
|
|
238
|
+
} finally {
|
|
239
|
+
Date.now = originalDateNow;
|
|
240
|
+
jest.useRealTimers();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
211
244
|
test("applies repeated stream quotes that refresh quote freshness", async () => {
|
|
212
245
|
const realDateNow = Date.now;
|
|
213
246
|
const { provider, emitQuote } = createProvider();
|
|
@@ -245,6 +278,45 @@ describe("MarketDataCoordinator key subscriptions", () => {
|
|
|
245
278
|
}
|
|
246
279
|
});
|
|
247
280
|
|
|
281
|
+
test("advances receipt freshness for an identical live stream heartbeat", async () => {
|
|
282
|
+
const realDateNow = Date.now;
|
|
283
|
+
const { provider, emitQuote } = createProvider();
|
|
284
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
285
|
+
const option = { symbol: "AAPL260731C00110000", exchange: "OPTIONS" };
|
|
286
|
+
const firstTimestamp = 1_800_000_000_000;
|
|
287
|
+
let calls = 0;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
coordinator.subscribeKeys([buildQuoteKey(option)], () => { calls += 1; });
|
|
291
|
+
coordinator.subscribeQuotes([{ instrument: option }]);
|
|
292
|
+
|
|
293
|
+
Date.now = () => firstTimestamp;
|
|
294
|
+
const heartbeat = quote(option.symbol, 2.5, {
|
|
295
|
+
lastUpdated: firstTimestamp,
|
|
296
|
+
bid: 2.4,
|
|
297
|
+
ask: 2.6,
|
|
298
|
+
mark: 2.5,
|
|
299
|
+
dataSource: "live",
|
|
300
|
+
delivery: "stream",
|
|
301
|
+
stale: false,
|
|
302
|
+
});
|
|
303
|
+
emitQuote(option, heartbeat);
|
|
304
|
+
await flushCoordinator();
|
|
305
|
+
|
|
306
|
+
Date.now = () => firstTimestamp + 60_000;
|
|
307
|
+
emitQuote(option, heartbeat);
|
|
308
|
+
await flushCoordinator();
|
|
309
|
+
|
|
310
|
+
expect(calls).toBe(2);
|
|
311
|
+
expect(coordinator.getQuoteEntry(option).data).toMatchObject({
|
|
312
|
+
lastUpdated: firstTimestamp,
|
|
313
|
+
receivedAt: firstTimestamp + 60_000,
|
|
314
|
+
});
|
|
315
|
+
} finally {
|
|
316
|
+
Date.now = realDateNow;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
248
320
|
test("does not refresh receipt time from an explicitly stale polled quote", async () => {
|
|
249
321
|
const realDateNow = Date.now;
|
|
250
322
|
const { provider, emitQuote } = createProvider();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { usePaneFooter, type PaneHint } from "../../../components";
|
|
3
3
|
import type { AccountProfile } from "../../../api-client";
|
|
4
|
+
import { resolvePlanAccess } from "../shared/plan-access";
|
|
4
5
|
import { formatPlan, type AccountDraft } from "./model";
|
|
5
|
-
import { t } from "../../../i18n";
|
|
6
|
+
import { t, tf } from "../../../i18n";
|
|
6
7
|
import { useAppLanguage } from "../../../i18n/react";
|
|
7
8
|
|
|
8
9
|
export function useAccountManagementFooter({
|
|
@@ -25,15 +26,20 @@ export function useAccountManagementFooter({
|
|
|
25
26
|
{ id: "save", key: "Ctrl+S", label: t("save"), onPress: () => { void saveProfile(); }, disabled: !!busy || !hasSession },
|
|
26
27
|
], [busy, hasSession, language, saveProfile]);
|
|
27
28
|
|
|
29
|
+
const planAccess = resolvePlanAccess(profile);
|
|
30
|
+
const planText = planAccess.isTrialActive
|
|
31
|
+
? tf("Pro trial · {days}d left", { days: planAccess.trialDaysLeft })
|
|
32
|
+
: formatPlan(profile?.plan);
|
|
33
|
+
|
|
28
34
|
usePaneFooter("account-management", () => ({
|
|
29
35
|
info: [
|
|
30
36
|
...(message ? [{ id: "status", parts: [{ text: message.text, tone: message.tone === "error" ? "negative" as const : message.tone === "success" ? "positive" as const : "muted" as const }] }] : []),
|
|
31
37
|
...(profile ? [
|
|
32
38
|
{ id: "account", parts: [{ text: profile.email, tone: "muted" as const }] },
|
|
33
|
-
{ id: "plan", parts: [{ text:
|
|
39
|
+
{ id: "plan", parts: [{ text: planText, tone: planAccess.hasProAccess ? "positive" as const : "muted" as const }] },
|
|
34
40
|
{ id: "visibility", parts: [{ text: draft.profilePublic ? t("public") : t("private"), tone: "muted" as const }] },
|
|
35
41
|
] : []),
|
|
36
42
|
],
|
|
37
43
|
hints: footerHints,
|
|
38
|
-
}), [draft.profilePublic, footerHints, language, message, profile]);
|
|
44
|
+
}), [draft.profilePublic, footerHints, language, message, planAccess.hasProAccess, planText, profile]);
|
|
39
45
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChoiceDialogChoice } from "../../../components";
|
|
2
|
-
import type { AccountProfile, PublicPortfolioAnalytics } from "../../../api-client";
|
|
2
|
+
import type { AccountProfile, CloudPricing, PublicPortfolioAnalytics } from "../../../api-client";
|
|
3
3
|
import type { Portfolio, TickerRecord } from "../../../types/ticker";
|
|
4
4
|
import { formatNumber } from "../../../utils/format";
|
|
5
5
|
import { t, tf } from "../../../i18n";
|
|
@@ -86,6 +86,58 @@ export function formatPlan(plan: AccountProfile["plan"] | null | undefined): str
|
|
|
86
86
|
return plan === "pro" ? t("Pro") : t("Free");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const DEFAULT_TRIAL_DAYS = 7;
|
|
90
|
+
|
|
91
|
+
export interface PlanPriceDisplay {
|
|
92
|
+
/** Charged price, already suffixed with the billing period. */
|
|
93
|
+
price: string;
|
|
94
|
+
/** List price to strike through, or null when the list price is the price. */
|
|
95
|
+
anchor: string | null;
|
|
96
|
+
note: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatUsdCents(cents: number): string {
|
|
100
|
+
const dollars = cents / 100;
|
|
101
|
+
return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Monthly Pro price for the plan comparison. Falls back to the list price when
|
|
106
|
+
* `/pricing` is unreachable so the table never renders a blank cell.
|
|
107
|
+
*/
|
|
108
|
+
export function formatCloudMonthlyPrice(pricing: CloudPricing | null | undefined): PlanPriceDisplay {
|
|
109
|
+
if (!pricing) return { price: t("$49/mo"), anchor: null, note: null };
|
|
110
|
+
const monthly = pricing.monthly;
|
|
111
|
+
// Without the founding discount the list price is the price, shown plainly.
|
|
112
|
+
if (!pricing.founding || monthly.anchorAmount <= monthly.amount) {
|
|
113
|
+
return {
|
|
114
|
+
price: tf("{amount}/mo", { amount: formatUsdCents(monthly.anchorAmount) }),
|
|
115
|
+
anchor: null,
|
|
116
|
+
note: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
price: tf("{amount}/mo", { amount: formatUsdCents(monthly.amount) }),
|
|
121
|
+
anchor: tf("{amount}/mo", { amount: formatUsdCents(monthly.anchorAmount) }),
|
|
122
|
+
note: t("Founding price"),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Short calendar date for the trial countdown, e.g. "Aug 11". */
|
|
127
|
+
export function formatTrialEnd(endsAt: Date | null | undefined): string | null {
|
|
128
|
+
if (!endsAt || Number.isNaN(endsAt.getTime())) return null;
|
|
129
|
+
return endsAt.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pro-column note for the plan comparison, e.g. "7-day free trial". The trial
|
|
134
|
+
* is part of the Pro subscription (card required, cancel before it converts),
|
|
135
|
+
* so it is never advertised as something the free plan includes.
|
|
136
|
+
*/
|
|
137
|
+
export function formatTrialOffer(pricing: CloudPricing | null | undefined): string {
|
|
138
|
+
return tf("{days}-day free trial", { days: pricing?.trialDays ?? DEFAULT_TRIAL_DAYS });
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
export function countPortfolioHoldings(tickers: ReadonlyMap<string, TickerRecord>): Record<string, number> {
|
|
90
142
|
const counts: Record<string, number> = {};
|
|
91
143
|
for (const record of tickers.values()) {
|
|
@@ -8,7 +8,7 @@ import type { PaneProps } from "../../../types/plugin";
|
|
|
8
8
|
import { Box, ScrollBox, Text, Textarea, TextAttributes, type TextareaRenderable, useRendererHost, useUiHost } from "../../../ui";
|
|
9
9
|
import { useDialog, type AlertContext, type PromptContext } from "../../../ui/dialog";
|
|
10
10
|
import { openNativeSelect, type NativeSelectElement } from "../../../components/ui/native-select";
|
|
11
|
-
import { apiClient, type AccountProfile } from "../../../api-client";
|
|
11
|
+
import { apiClient, type AccountProfile, type CloudPricing } from "../../../api-client";
|
|
12
12
|
import { chatController } from "../chat/controller";
|
|
13
13
|
import { CloudAuthNotice } from "../cloud/auth-actions";
|
|
14
14
|
import {
|
|
@@ -26,13 +26,17 @@ import {
|
|
|
26
26
|
computeCumulativeReturn,
|
|
27
27
|
countPortfolioHoldings,
|
|
28
28
|
emptyToNull,
|
|
29
|
+
formatCloudMonthlyPrice,
|
|
29
30
|
formatPlan,
|
|
31
|
+
formatTrialEnd,
|
|
32
|
+
formatTrialOffer,
|
|
30
33
|
getPortfolioPositionTickers,
|
|
31
34
|
portfolioOptionIds,
|
|
32
35
|
profileToDraft,
|
|
33
36
|
selectedPortfolioLabel,
|
|
34
37
|
type AccountDraft,
|
|
35
38
|
type AccountFieldKey,
|
|
39
|
+
type PlanPriceDisplay,
|
|
36
40
|
} from "./model";
|
|
37
41
|
import { PasswordChangeDialog } from "./password-dialog";
|
|
38
42
|
import { useAccountManagementFooter } from "./footer";
|
|
@@ -47,7 +51,7 @@ import { computeDatedBeta } from "../analytics/metrics";
|
|
|
47
51
|
import { useCloudSyncStatus } from "../../../sync/react";
|
|
48
52
|
import { cloudSyncController } from "../../../sync/controller";
|
|
49
53
|
import { setSyncedProfileAnalytics } from "../../../sync/profile-analytics";
|
|
50
|
-
import { t } from "../../../i18n";
|
|
54
|
+
import { t, tf } from "../../../i18n";
|
|
51
55
|
import { useAppLanguage } from "../../../i18n/react";
|
|
52
56
|
import {
|
|
53
57
|
consumeRequestedAccountManagementTab,
|
|
@@ -55,6 +59,7 @@ import {
|
|
|
55
59
|
type AccountManagementTab,
|
|
56
60
|
} from "./navigation";
|
|
57
61
|
import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
|
|
62
|
+
import { resolvePlanAccess } from "../shared/plan-access";
|
|
58
63
|
|
|
59
64
|
type AccountBusy = "profile" | "password" | "alerts" | "billing" | "delete" | null;
|
|
60
65
|
const ACCOUNT_TAB_DEFS: Array<{ label: string; value: AccountManagementTab }> = [
|
|
@@ -91,6 +96,7 @@ const PLAN_COMPARISON_ROWS = [
|
|
|
91
96
|
{ capability: "US equities", free: "15m delay", pro: "Real-time", proTone: "positive" },
|
|
92
97
|
{ capability: "Options data", free: "15m delay", pro: "Real-time", proTone: "positive" },
|
|
93
98
|
{ capability: "News", free: "12h delay", pro: "Real-time wire", proTone: "positive" },
|
|
99
|
+
{ capability: "AI command bar", free: "Included", pro: "Included", proTone: "neutral" },
|
|
94
100
|
{ capability: "Cloud sync", free: "Included", pro: "Included", proTone: "neutral" },
|
|
95
101
|
{ capability: "X data", free: "No", pro: "Included", proTone: "positive" },
|
|
96
102
|
{ capability: "AI Screener", free: "No", pro: "Soon", proTone: "muted" },
|
|
@@ -99,10 +105,15 @@ const PLAN_COMPARISON_ROWS = [
|
|
|
99
105
|
function PlanComparison({
|
|
100
106
|
width,
|
|
101
107
|
activePlan,
|
|
108
|
+
proNote,
|
|
109
|
+
price,
|
|
102
110
|
upgradeButton,
|
|
103
111
|
}: {
|
|
104
112
|
width: number;
|
|
105
113
|
activePlan: "free" | "pro";
|
|
114
|
+
/** Pro-column detail such as the free-trial offer; null once entitled. */
|
|
115
|
+
proNote: string | null;
|
|
116
|
+
price: PlanPriceDisplay;
|
|
106
117
|
upgradeButton: ReactNode;
|
|
107
118
|
}) {
|
|
108
119
|
const isDesktop = useUiHost().kind === "desktop-web";
|
|
@@ -162,17 +173,34 @@ function PlanComparison({
|
|
|
162
173
|
>
|
|
163
174
|
{t("Free")}
|
|
164
175
|
</Text>
|
|
165
|
-
<Box flexDirection="
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
+
<Box flexDirection="column">
|
|
177
|
+
<Box flexDirection="row" alignItems="baseline" gap={1}>
|
|
178
|
+
<Text
|
|
179
|
+
fg={colors.borderFocused}
|
|
180
|
+
attributes={TextAttributes.BOLD}
|
|
181
|
+
style={{ ...desktopText, fontWeight: 750 }}
|
|
182
|
+
>
|
|
183
|
+
{t("Pro")}
|
|
184
|
+
</Text>
|
|
185
|
+
{price.anchor ? (
|
|
186
|
+
<Text
|
|
187
|
+
fg={colors.textMuted}
|
|
188
|
+
attributes={TextAttributes.STRIKETHROUGH}
|
|
189
|
+
style={desktopText}
|
|
190
|
+
>
|
|
191
|
+
{price.anchor}
|
|
192
|
+
</Text>
|
|
193
|
+
) : null}
|
|
194
|
+
<Text fg={colors.textBright} style={desktopText}>
|
|
195
|
+
{price.price}
|
|
196
|
+
</Text>
|
|
197
|
+
{price.note ? (
|
|
198
|
+
<Text fg={colors.positive} style={desktopText}>{price.note}</Text>
|
|
199
|
+
) : null}
|
|
200
|
+
</Box>
|
|
201
|
+
{proNote ? (
|
|
202
|
+
<Text fg={colors.textMuted} style={desktopText}>{proNote}</Text>
|
|
203
|
+
) : null}
|
|
176
204
|
</Box>
|
|
177
205
|
</Box>
|
|
178
206
|
{PLAN_COMPARISON_ROWS.map((row, index) => {
|
|
@@ -260,7 +288,7 @@ function PlanComparison({
|
|
|
260
288
|
</Text>
|
|
261
289
|
<Box width={valueWidth} backgroundColor={proCellBg} paddingX={1}>
|
|
262
290
|
<Text fg={proHeadingFg} attributes={TextAttributes.BOLD}>
|
|
263
|
-
{`${t("Pro")} ${
|
|
291
|
+
{`${t("Pro")} ${price.price}`}
|
|
264
292
|
</Text>
|
|
265
293
|
</Box>
|
|
266
294
|
</Box>
|
|
@@ -275,6 +303,21 @@ function PlanComparison({
|
|
|
275
303
|
</Box>
|
|
276
304
|
</Box>
|
|
277
305
|
))}
|
|
306
|
+
{price.anchor ? (
|
|
307
|
+
<Box height={1} flexDirection="row" gap={1}>
|
|
308
|
+
<Text fg={colors.textMuted} attributes={TextAttributes.STRIKETHROUGH}>{price.anchor}</Text>
|
|
309
|
+
{price.note ? <Text fg={colors.positive}>{price.note}</Text> : null}
|
|
310
|
+
</Box>
|
|
311
|
+
) : null}
|
|
312
|
+
{proNote ? (
|
|
313
|
+
<Box height={1} flexDirection="row" gap={1}>
|
|
314
|
+
<Box width={capabilityWidth} />
|
|
315
|
+
<Box width={valueWidth} />
|
|
316
|
+
<Box width={valueWidth} paddingX={1}>
|
|
317
|
+
<Text fg={colors.textMuted}>{proNote}</Text>
|
|
318
|
+
</Box>
|
|
319
|
+
</Box>
|
|
320
|
+
) : null}
|
|
278
321
|
<Box height={1} flexDirection="row" gap={1}>
|
|
279
322
|
<Box width={capabilityWidth} />
|
|
280
323
|
<Box width={valueWidth} />
|
|
@@ -286,6 +329,22 @@ function PlanComparison({
|
|
|
286
329
|
);
|
|
287
330
|
}
|
|
288
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Reloads the profile whenever the account identity *or* its entitlement changes,
|
|
334
|
+
* so a completed checkout shows the new plan as soon as the session refreshes.
|
|
335
|
+
*/
|
|
336
|
+
function buildAccountSessionMarker(): string {
|
|
337
|
+
const snapshot = chatController.getSnapshot();
|
|
338
|
+
const user = apiClient.getCurrentUser();
|
|
339
|
+
return [
|
|
340
|
+
apiClient.getSessionToken() ?? "",
|
|
341
|
+
snapshot.user?.id ?? "",
|
|
342
|
+
snapshot.user?.username ?? "",
|
|
343
|
+
user?.effectivePlan ?? user?.plan ?? "",
|
|
344
|
+
user?.trialEndsAt ?? "",
|
|
345
|
+
].join(":");
|
|
346
|
+
}
|
|
347
|
+
|
|
289
348
|
export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
290
349
|
const language = useAppLanguage();
|
|
291
350
|
const dialog = useDialog();
|
|
@@ -297,12 +356,10 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
297
356
|
const tickers = useAppSelector((state) => state.tickers);
|
|
298
357
|
const cachedFinancials = useAppSelector((state) => state.financials);
|
|
299
358
|
const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
|
|
300
|
-
const [sessionMarker, setSessionMarker] = useState(() =>
|
|
301
|
-
const snapshot = chatController.getSnapshot();
|
|
302
|
-
return `${apiClient.getSessionToken() ?? ""}:${snapshot.user?.id ?? ""}:${snapshot.user?.username ?? ""}`;
|
|
303
|
-
});
|
|
359
|
+
const [sessionMarker, setSessionMarker] = useState(() => buildAccountSessionMarker());
|
|
304
360
|
const [hasSession, setHasSession] = useState(() => !!apiClient.getSessionToken());
|
|
305
361
|
const [profile, setProfile] = useState<AccountProfile | null>(null);
|
|
362
|
+
const [pricing, setPricing] = useState<CloudPricing | null>(null);
|
|
306
363
|
const [draft, setDraft] = useState<AccountDraft>(() => profileToDraft(null));
|
|
307
364
|
const [initialTab] = useState<AccountManagementTab>(
|
|
308
365
|
() => consumeRequestedAccountManagementTab() ?? "profile",
|
|
@@ -334,6 +391,14 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
334
391
|
const fieldOrder = ACCOUNT_TAB_FIELD_ORDER[activeTab];
|
|
335
392
|
const accountTabs = ACCOUNT_TAB_DEFS.map((tab) => ({ ...tab, label: t(tab.label) }));
|
|
336
393
|
|
|
394
|
+
const planAccess = useMemo(() => resolvePlanAccess(profile), [profile]);
|
|
395
|
+
const planPrice = useMemo(() => formatCloudMonthlyPrice(pricing), [language, pricing]);
|
|
396
|
+
const trialOffer = useMemo(() => formatTrialOffer(pricing), [language, pricing]);
|
|
397
|
+
const planStatusLabel = useMemo(() => {
|
|
398
|
+
const trialEnd = planAccess.isTrialActive ? formatTrialEnd(planAccess.trialEndsAt) : null;
|
|
399
|
+
return trialEnd ? tf("Pro trial — ends {date}", { date: trialEnd }) : formatPlan(profile?.plan);
|
|
400
|
+
}, [language, planAccess.isTrialActive, planAccess.trialEndsAt, profile?.plan]);
|
|
401
|
+
|
|
337
402
|
const portfolioHoldingCounts = useMemo(() => countPortfolioHoldings(tickers), [tickers]);
|
|
338
403
|
const portfolioChoices = useMemo(
|
|
339
404
|
() => buildPortfolioChoices(portfolios, portfolioHoldingCounts),
|
|
@@ -481,7 +546,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
481
546
|
useEffect(() => {
|
|
482
547
|
const unsubscribe = chatController.subscribe((snapshot) => {
|
|
483
548
|
setHasSession(!!apiClient.getSessionToken() || snapshot.hasSavedSession);
|
|
484
|
-
setSessionMarker(
|
|
549
|
+
setSessionMarker(buildAccountSessionMarker());
|
|
485
550
|
});
|
|
486
551
|
void chatController.refreshSession().catch(() => {});
|
|
487
552
|
return unsubscribe;
|
|
@@ -512,6 +577,19 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
512
577
|
void loadProfile();
|
|
513
578
|
}, [loadProfile, sessionMarker]);
|
|
514
579
|
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
let cancelled = false;
|
|
582
|
+
void apiClient.getCloudPricing()
|
|
583
|
+
.then((nextPricing) => {
|
|
584
|
+
if (!cancelled) setPricing(nextPricing);
|
|
585
|
+
})
|
|
586
|
+
// Pricing is decoration: the comparison table falls back to list prices.
|
|
587
|
+
.catch(() => {});
|
|
588
|
+
return () => {
|
|
589
|
+
cancelled = true;
|
|
590
|
+
};
|
|
591
|
+
}, []);
|
|
592
|
+
|
|
515
593
|
useEffect(() => {
|
|
516
594
|
if (!hasSession || !apiClient.getSessionToken()) return;
|
|
517
595
|
if (syncStatus.phase !== "synced" || syncStatus.revision == null) return;
|
|
@@ -1002,18 +1080,23 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
|
|
|
1002
1080
|
<>
|
|
1003
1081
|
<Box flexDirection="row" gap={1}>
|
|
1004
1082
|
<Text fg={colors.textDim}>{t("Status")}</Text>
|
|
1005
|
-
<Text
|
|
1006
|
-
{
|
|
1083
|
+
<Text
|
|
1084
|
+
fg={planAccess.hasProAccess ? colors.positive : colors.textBright}
|
|
1085
|
+
attributes={TextAttributes.BOLD}
|
|
1086
|
+
>
|
|
1087
|
+
{planStatusLabel}
|
|
1007
1088
|
</Text>
|
|
1008
1089
|
{profile?.email ? <Text fg={colors.textMuted}>{profile.email}</Text> : null}
|
|
1009
1090
|
</Box>
|
|
1010
1091
|
<PlanComparison
|
|
1011
1092
|
width={contentWidth}
|
|
1012
|
-
activePlan={
|
|
1093
|
+
activePlan={planAccess.hasProAccess ? "pro" : "free"}
|
|
1094
|
+
proNote={planAccess.hasProAccess ? null : trialOffer}
|
|
1095
|
+
price={planPrice}
|
|
1013
1096
|
upgradeButton={(
|
|
1014
1097
|
<Button
|
|
1015
|
-
label={
|
|
1016
|
-
variant={
|
|
1098
|
+
label={planAccess.isPayingPro ? t("Manage Pro") : busy === "billing" ? t("Opening...") : t("Upgrade to Pro")}
|
|
1099
|
+
variant={planAccess.isPayingPro ? "secondary" : "primary"}
|
|
1017
1100
|
width={isDesktop ? 28 : undefined}
|
|
1018
1101
|
height={isDesktop ? "28px" : undefined}
|
|
1019
1102
|
active={activeField === "upgradeAction"}
|
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
normalizeTabs,
|
|
47
47
|
type PersistedAiScreenerPaneState,
|
|
48
48
|
} from "./screener/model";
|
|
49
|
+
import {
|
|
50
|
+
LIVE_STREAMING_QUICK_SETTING,
|
|
51
|
+
withLiveStreamingSetting,
|
|
52
|
+
} from "../shared/live-streaming";
|
|
49
53
|
|
|
50
54
|
function settingOrFallback(
|
|
51
55
|
settings: Record<string, unknown>,
|
|
@@ -331,6 +335,7 @@ export const aiPlugin: GloomPlugin = {
|
|
|
331
335
|
defaultPosition: "right",
|
|
332
336
|
defaultMode: "floating",
|
|
333
337
|
defaultFloatingSize: { width: 76, height: 24 },
|
|
338
|
+
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
334
339
|
settings: (context) => {
|
|
335
340
|
const providers = detectProviders();
|
|
336
341
|
const screenerProviders = providers.filter((provider) => (
|
|
@@ -351,7 +356,7 @@ export const aiPlugin: GloomPlugin = {
|
|
|
351
356
|
? context.paneState.activeTabId
|
|
352
357
|
: null;
|
|
353
358
|
const activeTab = tabs.find((tab) => tab.id === activeTabId) ?? tabs[0] ?? null;
|
|
354
|
-
return buildAiPaneSettingsDef({
|
|
359
|
+
return withLiveStreamingSetting(buildAiPaneSettingsDef({
|
|
355
360
|
title: "AI Screener Settings",
|
|
356
361
|
providers: settingsProviders(screenerProviders),
|
|
357
362
|
models: settingsModels(providerIds),
|
|
@@ -369,7 +374,7 @@ export const aiPlugin: GloomPlugin = {
|
|
|
369
374
|
additional: buildAiScreenerPaneSettingsDef(
|
|
370
375
|
getAiScreenerPaneSettings(context.settings),
|
|
371
376
|
),
|
|
372
|
-
});
|
|
377
|
+
}), context.settings);
|
|
373
378
|
},
|
|
374
379
|
});
|
|
375
380
|
|
|
@@ -5,7 +5,7 @@ import { useAppSelector } from "../../../../state/app/context";
|
|
|
5
5
|
import { useFxRatesMap, useTickerFinancialsMap } from "../../../../market-data/hooks";
|
|
6
6
|
import { getSharedMarketDataCoordinator } from "../../../../market-data/coordinator";
|
|
7
7
|
import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../../../market-data/request-types";
|
|
8
|
-
import {
|
|
8
|
+
import { useQuoteUpdates } from "../../../../state/hooks/quote-streaming";
|
|
9
9
|
import type { ColumnContext } from "../../portfolio-list/metrics";
|
|
10
10
|
import type { ColumnConfig } from "../../../../types/config";
|
|
11
11
|
import type { ValidatedScreenerResult } from "./contract";
|
|
@@ -21,6 +21,7 @@ interface UseAiScreenerMarketRuntimeOptions {
|
|
|
21
21
|
resultMap: Map<string, ValidatedScreenerResult>;
|
|
22
22
|
setCursorSymbol: (symbol: string | null) => void;
|
|
23
23
|
tickers: Map<string, TickerRecord>;
|
|
24
|
+
liveStreaming: boolean;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function useAiScreenerMarketRuntime({
|
|
@@ -32,6 +33,7 @@ export function useAiScreenerMarketRuntime({
|
|
|
32
33
|
resultMap,
|
|
33
34
|
setCursorSymbol,
|
|
34
35
|
tickers,
|
|
36
|
+
liveStreaming,
|
|
35
37
|
}: UseAiScreenerMarketRuntimeOptions) {
|
|
36
38
|
const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
|
|
37
39
|
const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
|
|
@@ -68,7 +70,7 @@ export function useAiScreenerMarketRuntime({
|
|
|
68
70
|
})
|
|
69
71
|
), [sortedTickers]);
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
useQuoteUpdates(quoteTargets, { liveStreaming });
|
|
72
74
|
|
|
73
75
|
useEffect(() => {
|
|
74
76
|
const coordinator = getSharedMarketDataCoordinator();
|
|
@@ -52,12 +52,14 @@ import {
|
|
|
52
52
|
AI_DEFAULT_PROVIDER_SETTING_KEY,
|
|
53
53
|
resolveAiPaneSelection,
|
|
54
54
|
} from "../pane-settings";
|
|
55
|
+
import { useLiveStreamingSetting } from "../../shared/live-streaming";
|
|
55
56
|
|
|
56
57
|
export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
57
58
|
const dataProvider = useAssetData();
|
|
58
59
|
const { pinTicker } = usePluginTickerActions();
|
|
59
60
|
const paneId = usePaneInstanceId();
|
|
60
61
|
const paneInstance = usePaneInstance();
|
|
62
|
+
const liveStreaming = useLiveStreamingSetting();
|
|
61
63
|
const dispatch = useAppDispatch();
|
|
62
64
|
const tickers = useAppSelector((state) => state.tickers);
|
|
63
65
|
const providers = useAiRuntimeProviders();
|
|
@@ -169,6 +171,7 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
169
171
|
resultMap,
|
|
170
172
|
setCursorSymbol,
|
|
171
173
|
tickers,
|
|
174
|
+
liveStreaming,
|
|
172
175
|
});
|
|
173
176
|
|
|
174
177
|
const updateTabs = useCallback((updater: (tabs: AiScreenerTab[]) => AiScreenerTab[]) => {
|
|
@@ -144,7 +144,6 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
|
|
|
144
144
|
: undefined}
|
|
145
145
|
scrollable={items.length > (isDesktop ? 5 : 7)}
|
|
146
146
|
rowGap={isDesktop ? 0 : undefined}
|
|
147
|
-
selectOnHover={isDesktop}
|
|
148
147
|
onSelect={setSelectedIndex}
|
|
149
148
|
onActivate={(_, index) => setSelectedIndex(index)}
|
|
150
149
|
/>
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
} from "./presets";
|
|
21
21
|
import { buildChartComposerPaneSettingsDef } from "./settings";
|
|
22
22
|
import type { PluginModule } from "../plugin-module";
|
|
23
|
+
import {
|
|
24
|
+
LIVE_STREAMING_QUICK_SETTING,
|
|
25
|
+
withLiveStreamingSetting,
|
|
26
|
+
} from "../shared/live-streaming";
|
|
23
27
|
|
|
24
28
|
const CHART_COMPOSER_TEMPLATE_ID = "chart-composer-pane";
|
|
25
29
|
|
|
@@ -199,9 +203,13 @@ export const chartComposerModule: PluginModule = {
|
|
|
199
203
|
defaultPosition: "right",
|
|
200
204
|
defaultMode: "floating",
|
|
201
205
|
defaultFloatingSize: { width: 100, height: 32 },
|
|
202
|
-
|
|
206
|
+
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
207
|
+
settings: (context) => withLiveStreamingSetting(
|
|
208
|
+
buildChartComposerPaneSettingsDef(
|
|
209
|
+
context.settings,
|
|
210
|
+
context.activeTicker,
|
|
211
|
+
),
|
|
203
212
|
context.settings,
|
|
204
|
-
context.activeTicker,
|
|
205
213
|
),
|
|
206
214
|
}],
|
|
207
215
|
paneTemplates: chartComposerTemplates,
|