gloomberb 0.12.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
package/README.md
CHANGED
|
@@ -137,6 +137,8 @@ Open command mode with `Ctrl+P`, then type a command. Press `` ` `` to open tick
|
|
|
137
137
|
|
|
138
138
|
Use **New Portfolio** or **Add Broker Account** to connect a broker. Gloomberb can import positions from Interactive Brokers, Public, Robinhood, and SimpleFIN.
|
|
139
139
|
|
|
140
|
+
Each broker is a plugin with its own repository, installed on first launch and updatable on its own. Manage them from the plugin directory, or with `gloomberb install gloom-sh/gloomberb-public` and friends.
|
|
141
|
+
|
|
140
142
|
- Robinhood opens a browser sign-in page. Gloomberb uses only the read-only account and equity-position tools from the Robinhood Trading MCP server.
|
|
141
143
|
- Public needs an API secret from Public API settings. Gloomberb creates a short-lived access token and uses only the account and portfolio endpoints.
|
|
142
144
|
- SimpleFIN needs a one-time setup token from SimpleFIN Bridge. Gloomberb exchanges the token and imports only accounts that contain holdings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gloomberb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"packageManager": "bun@1.3.11",
|
|
5
5
|
"description": "Market research and portfolio tracker for the terminal",
|
|
6
6
|
"module": "src/index.tsx",
|
|
@@ -94,7 +94,6 @@
|
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"@earendil-works/pi-agent-core": "0.81.1",
|
|
96
96
|
"@earendil-works/pi-ai": "0.81.1",
|
|
97
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
98
97
|
"@opentui/core": "^0.3.2",
|
|
99
98
|
"@opentui/react": "^0.3.2",
|
|
100
99
|
"@stoqey/ib": "^1.5.6",
|
package/src/api-client/auth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCurrentPluginTarget } from "../plugins/current-target";
|
|
1
2
|
import { ApiRequestError, isHardSessionInvalidMessage } from "./errors";
|
|
2
3
|
import type {
|
|
3
4
|
AccountProfile,
|
|
@@ -151,7 +152,7 @@ export class CloudAuthApi {
|
|
|
151
152
|
async sendVerification(): Promise<CloudVerificationResponse> {
|
|
152
153
|
return this.options.request<CloudVerificationResponse>("/cloud/auth/send-verification", {
|
|
153
154
|
method: "POST",
|
|
154
|
-
body: JSON.stringify({}),
|
|
155
|
+
body: JSON.stringify({ returnTo: getCurrentPluginTarget() === "web" ? location.href : undefined }),
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -264,6 +264,40 @@ describe("apiClient auth cookies", () => {
|
|
|
264
264
|
]);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
test("ignores a stale response cookie after the session credential changes", async () => {
|
|
268
|
+
let releaseResponse: (() => void) | null = null;
|
|
269
|
+
let markRequestStarted: (() => void) | null = null;
|
|
270
|
+
const requestStarted = new Promise<void>((resolve) => {
|
|
271
|
+
markRequestStarted = resolve;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
apiClient.setSessionToken("old-session.value");
|
|
275
|
+
setCloudApiFetchTransport(async () => {
|
|
276
|
+
markRequestStarted?.();
|
|
277
|
+
await new Promise<void>((resolve) => {
|
|
278
|
+
releaseResponse = resolve;
|
|
279
|
+
});
|
|
280
|
+
return createResponse({
|
|
281
|
+
channels: [],
|
|
282
|
+
onlineCount: 0,
|
|
283
|
+
channelStates: [],
|
|
284
|
+
notifications: [],
|
|
285
|
+
}, {
|
|
286
|
+
cookies: [
|
|
287
|
+
"__Secure-gloomberb.session_token=old-session.value; Path=/; HttpOnly; Secure; SameSite=None",
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const staleRequest = apiClient.getChatState();
|
|
293
|
+
await requestStarted;
|
|
294
|
+
apiClient.setSessionToken("new-session.value");
|
|
295
|
+
releaseResponse?.();
|
|
296
|
+
await staleRequest;
|
|
297
|
+
|
|
298
|
+
expect(apiClient.getSessionToken()).toBe("new-session.value");
|
|
299
|
+
});
|
|
300
|
+
|
|
267
301
|
test("uses an installed cloud API fetch transport for auth cookie capture", async () => {
|
|
268
302
|
const seenCookies: Array<string | null> = [];
|
|
269
303
|
globalThis.fetch = mockFetch(async () => {
|
package/src/api-client/index.ts
CHANGED
|
@@ -362,8 +362,16 @@ class GloomApiClient {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
/** Creates a Stripe checkout session for Cloud Pro; the URL opens in a browser. */
|
|
365
|
-
async createCloudCheckout(): Promise<{ url: string }> {
|
|
366
|
-
return this.request<{ url: string }>("/stripe/checkout", { method: "POST", body: JSON.stringify({}) });
|
|
365
|
+
async createCloudCheckout(returnTo?: string): Promise<{ url: string }> {
|
|
366
|
+
return this.request<{ url: string }>("/stripe/checkout", { method: "POST", body: JSON.stringify({ returnTo }) });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async recordResearchActivity(payload: {
|
|
370
|
+
event: import("./research-activity").ResearchActivity; eventId: string;
|
|
371
|
+
surface: "web" | "desktop" | "tui" | "cli"; anonymousId?: string;
|
|
372
|
+
attribution?: Record<string, string>; feature?: import("./research-activity").ResearchFeature;
|
|
373
|
+
}): Promise<void> {
|
|
374
|
+
await this.request("/activity/research", { method: "POST", body: JSON.stringify(payload) });
|
|
367
375
|
}
|
|
368
376
|
|
|
369
377
|
/** Stores a verified user's public terminal snapshot or pane handoff. */
|
|
@@ -141,6 +141,7 @@ export class CloudApiRequestTransport {
|
|
|
141
141
|
headers.set("Content-Type", "application/json");
|
|
142
142
|
}
|
|
143
143
|
this.setSessionCookieHeader(headers);
|
|
144
|
+
const sessionTokenAtRequestStart = this.sessionToken;
|
|
144
145
|
headers.set("Origin", this.baseUrl);
|
|
145
146
|
|
|
146
147
|
const operation = `${options?.method ?? "GET"} ${path.split("?")[0]}`;
|
|
@@ -151,7 +152,9 @@ export class CloudApiRequestTransport {
|
|
|
151
152
|
credentials: "include",
|
|
152
153
|
});
|
|
153
154
|
throwIfRequestAborted(options?.signal);
|
|
154
|
-
this.
|
|
155
|
+
if (this.sessionToken === sessionTokenAtRequestStart) {
|
|
156
|
+
this.extractSessionCookie(res);
|
|
157
|
+
}
|
|
155
158
|
const text = await res.text();
|
|
156
159
|
throwIfRequestAborted(options?.signal);
|
|
157
160
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { apiClient } from "./index";
|
|
2
|
+
import { getCurrentPluginTarget } from "../plugins/current-target";
|
|
3
|
+
|
|
4
|
+
export type ResearchActivity = "workspace_opened" | "research_viewed" | "ticker_saved" | "pro_feature_used" | "upgrade_intent";
|
|
5
|
+
export type ResearchFeature = "overview" | "chart" | "financials" | "news" | "transcripts" | "search" | "research";
|
|
6
|
+
const sent = new Set<string>();
|
|
7
|
+
let anonymousId: string | undefined;
|
|
8
|
+
let attribution: Record<string, string> = {};
|
|
9
|
+
|
|
10
|
+
/** Hosted web analytics only. Native local use never creates an identifier. */
|
|
11
|
+
export function initializeBrowserResearchActivity(): void {
|
|
12
|
+
try {
|
|
13
|
+
if (navigator.doNotTrack === "1" || (navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl) return;
|
|
14
|
+
const url = new URL(location.href);
|
|
15
|
+
const incoming = url.searchParams.get("_gloom");
|
|
16
|
+
const stored = localStorage.getItem("gloomberb.web.anonymous-id");
|
|
17
|
+
anonymousId = [incoming, stored].find((value) => value && /^[a-f0-9-]{36}$/.test(value)) ?? crypto.randomUUID();
|
|
18
|
+
localStorage.setItem("gloomberb.web.anonymous-id", anonymousId);
|
|
19
|
+
const keys = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "twclid"];
|
|
20
|
+
const saved = JSON.parse(localStorage.getItem("gloomberb.web.attribution") ?? "{}");
|
|
21
|
+
const age = Date.now() - Date.parse(saved?.last_touch_at ?? "");
|
|
22
|
+
attribution = Number.isFinite(age) && age >= 0 && age < 30 * 24 * 60 * 60 * 1000 ? saved : {};
|
|
23
|
+
if (keys.some((key) => url.searchParams.has(key))) {
|
|
24
|
+
// A new campaign replaces the old click id instead of inheriting it.
|
|
25
|
+
attribution = { last_touch_at: new Date().toISOString() };
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
const value = url.searchParams.get(key);
|
|
28
|
+
if (value) attribution[key] = value.slice(0, 300);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
localStorage.setItem("gloomberb.web.attribution", JSON.stringify(attribution));
|
|
32
|
+
url.searchParams.delete("_gloom");
|
|
33
|
+
history.replaceState(history.state, "", url.href);
|
|
34
|
+
} catch { /* Private browsing must still work. */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Counts milestones once per feature/session/account, never their content. */
|
|
38
|
+
export function recordResearchActivity(event: ResearchActivity, feature?: ResearchFeature): void {
|
|
39
|
+
const target = getCurrentPluginTarget();
|
|
40
|
+
const user = apiClient.getCurrentUser();
|
|
41
|
+
if (target === "web" ? !anonymousId : !user) return;
|
|
42
|
+
const key = `${user?.id ?? "guest"}:${event}:${feature ?? ""}`;
|
|
43
|
+
if (sent.has(key)) return;
|
|
44
|
+
sent.add(key);
|
|
45
|
+
if (event !== "workspace_opened") recordResearchActivity("workspace_opened");
|
|
46
|
+
void apiClient.recordResearchActivity({ event, eventId: crypto.randomUUID(),
|
|
47
|
+
surface: target === "desktop" ? "desktop" : target,
|
|
48
|
+
anonymousId: target === "web" ? anonymousId : undefined,
|
|
49
|
+
attribution: target === "web" ? attribution : undefined, feature,
|
|
50
|
+
}).catch(() => { sent.delete(key); });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function researchUpgradeUrl(returnTo?: string): string {
|
|
54
|
+
const url = new URL("https://gloom.sh/cloud?upgrade=pro");
|
|
55
|
+
if (returnTo) url.searchParams.set("returnTo", returnTo);
|
|
56
|
+
if (anonymousId) url.searchParams.set("_gloom", anonymousId);
|
|
57
|
+
for (const [key, value] of Object.entries(attribution)) {
|
|
58
|
+
if (/^(utm_(source|medium|campaign|content|term)|twclid)$/.test(key)) url.searchParams.set(key, value);
|
|
59
|
+
}
|
|
60
|
+
return url.href;
|
|
61
|
+
}
|
package/src/api-client/types.ts
CHANGED
|
@@ -518,7 +518,8 @@ export interface CloudTranscriptTurnPayload {
|
|
|
518
518
|
company?: string;
|
|
519
519
|
text: string;
|
|
520
520
|
isQa: boolean;
|
|
521
|
-
|
|
521
|
+
/** Offset into the recording; null when the company published the transcript as a document. */
|
|
522
|
+
startSeconds: number | null;
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
export interface CloudTranscriptParticipantPayload {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
|
-
import { getPluginsDir } from "../../plugins/loader";
|
|
4
|
+
import { getPluginsDir, isPluginDirectory } from "../../plugins/loader";
|
|
5
5
|
import { linkHostPackages } from "../../plugins/host-link";
|
|
6
6
|
import {
|
|
7
7
|
cliStyles,
|
|
@@ -149,7 +149,7 @@ export async function updatePlugins(name?: string) {
|
|
|
149
149
|
const dirs = name
|
|
150
150
|
? [validatePluginDirectoryName(name)]
|
|
151
151
|
: readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
152
|
-
.filter((entry) => entry.isDirectory())
|
|
152
|
+
.filter((entry) => entry.isDirectory() && isPluginDirectory(entry.name))
|
|
153
153
|
.map((entry) => entry.name);
|
|
154
154
|
|
|
155
155
|
if (dirs.length === 0) {
|
|
@@ -179,7 +179,8 @@ export async function updatePlugins(name?: string) {
|
|
|
179
179
|
|
|
180
180
|
export function listPlugins() {
|
|
181
181
|
ensurePluginsDir();
|
|
182
|
-
const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
182
|
+
const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
183
|
+
.filter((entry) => entry.isDirectory() && isPluginDirectory(entry.name));
|
|
183
184
|
|
|
184
185
|
if (entries.length === 0) {
|
|
185
186
|
console.log(cliStyles.muted("No plugins installed."));
|
|
@@ -3,7 +3,8 @@ import { tmpdir } from "os";
|
|
|
3
3
|
import { join, resolve, sep } from "path";
|
|
4
4
|
import type { AppConfig } from "../types/config";
|
|
5
5
|
import type { ResolvedSeries } from "../time-series/types";
|
|
6
|
-
import type { OptionsChain, TickerFinancials } from "../types/financials";
|
|
6
|
+
import type { OptionsChain, PricePoint, TickerFinancials } from "../types/financials";
|
|
7
|
+
import type { ManualChartResolution } from "../time-series/resolution";
|
|
7
8
|
import type { TickerRecord } from "../types/ticker";
|
|
8
9
|
import type { PaneRuntimeState } from "../core/state/app/state";
|
|
9
10
|
import type { RemoteUiNodeSnapshot } from "../remote/types";
|
|
@@ -14,6 +15,19 @@ import {
|
|
|
14
15
|
writeElectrobunViewPage,
|
|
15
16
|
} from "../renderers/electrobun/view/build-assets";
|
|
16
17
|
|
|
18
|
+
export interface DesktopPaneShotIntradayHistory {
|
|
19
|
+
symbol: string;
|
|
20
|
+
exchange: string;
|
|
21
|
+
rangePreset: "1D" | "1W";
|
|
22
|
+
resolution: ManualChartResolution;
|
|
23
|
+
requestedSession: string | null;
|
|
24
|
+
sessionDates: string[];
|
|
25
|
+
points: PricePoint[];
|
|
26
|
+
start: string | null;
|
|
27
|
+
end: string | null;
|
|
28
|
+
unavailableReason: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export interface DesktopPaneShotPayload {
|
|
18
32
|
config: AppConfig;
|
|
19
33
|
paneId: string;
|
|
@@ -27,6 +41,7 @@ export interface DesktopPaneShotPayload {
|
|
|
27
41
|
watermark?: string | null;
|
|
28
42
|
tickers: TickerRecord[];
|
|
29
43
|
financials: Array<[string, TickerFinancials]>;
|
|
44
|
+
intradayHistories: DesktopPaneShotIntradayHistory[];
|
|
30
45
|
optionsChains: Array<[string, OptionsChain]>;
|
|
31
46
|
fredSeries: Array<[string, FredSeriesCacheEntry]>;
|
|
32
47
|
valuationSeries: Array<[string, DatedObservation[]]>;
|
|
@@ -58,6 +73,8 @@ export interface DesktopPaneShotRenderedRow {
|
|
|
58
73
|
export interface DesktopPaneShotRenderResult {
|
|
59
74
|
visibleText: string;
|
|
60
75
|
rows: DesktopPaneShotRenderedRow[];
|
|
76
|
+
truncated: boolean;
|
|
77
|
+
truncationReasons: string[];
|
|
61
78
|
loadingStateDetected: boolean;
|
|
62
79
|
errorStateDetected: boolean;
|
|
63
80
|
errorStateMarkers: string[];
|
|
@@ -98,6 +115,7 @@ export async function renderDesktopPaneScreenshot(
|
|
|
98
115
|
payload: DesktopPaneShotPayload,
|
|
99
116
|
outputPath: string,
|
|
100
117
|
apiProxy: DesktopPaneShotApiProxy,
|
|
118
|
+
options: { captureImage?: boolean } = {},
|
|
101
119
|
): Promise<DesktopPaneShotRenderResult> {
|
|
102
120
|
const tempDir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-"));
|
|
103
121
|
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
@@ -115,6 +133,7 @@ export async function renderDesktopPaneScreenshot(
|
|
|
115
133
|
heightPx: payload.heightPx,
|
|
116
134
|
deviceScaleFactor: payload.deviceScaleFactor ?? DEFAULT_DEVICE_SCALE_FACTOR,
|
|
117
135
|
userDataDir: join(tempDir, "chrome-profile"),
|
|
136
|
+
captureImage: options.captureImage !== false,
|
|
118
137
|
});
|
|
119
138
|
} finally {
|
|
120
139
|
server?.stop(true);
|
|
@@ -296,6 +315,7 @@ async function capturePageScreenshot({
|
|
|
296
315
|
heightPx,
|
|
297
316
|
deviceScaleFactor,
|
|
298
317
|
userDataDir,
|
|
318
|
+
captureImage,
|
|
299
319
|
}: {
|
|
300
320
|
chrome: string;
|
|
301
321
|
url: string;
|
|
@@ -304,6 +324,7 @@ async function capturePageScreenshot({
|
|
|
304
324
|
heightPx: number;
|
|
305
325
|
deviceScaleFactor: number;
|
|
306
326
|
userDataDir: string;
|
|
327
|
+
captureImage: boolean;
|
|
307
328
|
}): Promise<DesktopPaneShotRenderResult> {
|
|
308
329
|
await mkdir(userDataDir, { recursive: true });
|
|
309
330
|
const port = 43000 + Math.floor(Math.random() * 10000);
|
|
@@ -344,13 +365,15 @@ async function capturePageScreenshot({
|
|
|
344
365
|
});
|
|
345
366
|
await waitForShotReady(session);
|
|
346
367
|
const rendered = await readRenderedPaneState(session);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
368
|
+
if (captureImage) {
|
|
369
|
+
const screenshot = await session.send("Page.captureScreenshot", {
|
|
370
|
+
format: "png",
|
|
371
|
+
fromSurface: true,
|
|
372
|
+
captureBeyondViewport: false,
|
|
373
|
+
}) as { data?: string };
|
|
374
|
+
if (!screenshot.data) throw new Error("Chrome did not return screenshot data.");
|
|
375
|
+
await writeFile(outputPath, Uint8Array.from(Buffer.from(screenshot.data, "base64")));
|
|
376
|
+
}
|
|
354
377
|
return rendered;
|
|
355
378
|
} catch (error) {
|
|
356
379
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -373,6 +396,7 @@ const ERROR_STATE_PATTERNS = [
|
|
|
373
396
|
/\bCould not load\b/gi,
|
|
374
397
|
/\bSign in to\b/gi,
|
|
375
398
|
/\bVerify your email\b/gi,
|
|
399
|
+
/\brequires signup and email verification\b/gi,
|
|
376
400
|
/\bpart of Gloom Cloud Pro\b/gi,
|
|
377
401
|
/\bCloud API request failed\b/gi,
|
|
378
402
|
];
|
|
@@ -416,6 +440,25 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
416
440
|
};
|
|
417
441
|
const semanticTables = semanticUi.filter((node) => node && node.role === "table");
|
|
418
442
|
const rows = [];
|
|
443
|
+
const truncationReasons = new Set();
|
|
444
|
+
const markScrollTruncation = (element) => {
|
|
445
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) return;
|
|
446
|
+
if (
|
|
447
|
+
element.getAttribute("data-gloom-scrollbar-y") === "visible"
|
|
448
|
+
|| element.scrollHeight > element.clientHeight + 1
|
|
449
|
+
) {
|
|
450
|
+
truncationReasons.add("rows extend below the rendered viewport");
|
|
451
|
+
}
|
|
452
|
+
if (
|
|
453
|
+
element.getAttribute("data-gloom-scrollbar-x") === "visible"
|
|
454
|
+
|| element.scrollWidth > element.clientWidth + 1
|
|
455
|
+
) {
|
|
456
|
+
truncationReasons.add("columns extend beyond the rendered viewport");
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
[...root.querySelectorAll(
|
|
460
|
+
'[data-gloom-scrollbar-x="visible"], [data-gloom-scrollbar-y="visible"], [data-gloom-role="data-table-body-scroll"]',
|
|
461
|
+
)].forEach(markScrollTruncation);
|
|
419
462
|
[...root.querySelectorAll('[data-gloom-role="data-table"]')]
|
|
420
463
|
.filter(isVisible)
|
|
421
464
|
.forEach((table, tableIndex) => {
|
|
@@ -433,12 +476,24 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
433
476
|
const semanticRow = semanticRows[rowIndex] || {};
|
|
434
477
|
const cells = values.map((cell, cellIndex) => {
|
|
435
478
|
const semanticColumn = semanticColumns[cellIndex] || {};
|
|
479
|
+
const text = normalize(cell.innerText || cell.textContent);
|
|
480
|
+
const textNodes = [cell, ...cell.querySelectorAll('*')];
|
|
481
|
+
if (
|
|
482
|
+
/\\u2026|\\.\\.\\./.test(text)
|
|
483
|
+
|| textNodes.some((node) => (
|
|
484
|
+
node instanceof HTMLElement
|
|
485
|
+
&& isVisible(node)
|
|
486
|
+
&& node.scrollWidth > node.clientWidth + 1
|
|
487
|
+
))
|
|
488
|
+
) {
|
|
489
|
+
truncationReasons.add("one or more cells are visibly clipped");
|
|
490
|
+
}
|
|
436
491
|
return {
|
|
437
492
|
...(typeof semanticColumn.id === "string" ? { columnId: semanticColumn.id } : {}),
|
|
438
493
|
columnLabel: typeof semanticColumn.label === "string"
|
|
439
494
|
? semanticColumn.label
|
|
440
495
|
: headers[cellIndex] || (values.length === 1 ? "Row" : String(cellIndex + 1)),
|
|
441
|
-
text
|
|
496
|
+
text,
|
|
442
497
|
};
|
|
443
498
|
}).filter((cell) => cell.text.length > 0);
|
|
444
499
|
if (cells.length === 0) return;
|
|
@@ -472,6 +527,8 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
472
527
|
errorStateDetected: root.querySelector('[data-gloom-status="error"]') !== null,
|
|
473
528
|
emptyStateDetected: root.querySelector('[data-gloom-status="empty"]') !== null,
|
|
474
529
|
rows,
|
|
530
|
+
truncated: truncationReasons.size > 0,
|
|
531
|
+
truncationReasons: [...truncationReasons],
|
|
475
532
|
semanticUi,
|
|
476
533
|
};
|
|
477
534
|
})()`,
|
|
@@ -485,6 +542,8 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
485
542
|
errorStateDetected?: boolean;
|
|
486
543
|
emptyStateDetected?: boolean;
|
|
487
544
|
rows?: DesktopPaneShotRenderedRow[];
|
|
545
|
+
truncated?: boolean;
|
|
546
|
+
truncationReasons?: string[];
|
|
488
547
|
semanticUi?: RemoteUiNodeSnapshot[];
|
|
489
548
|
};
|
|
490
549
|
};
|
|
@@ -495,9 +554,19 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
495
554
|
const loadingStateMarkers = stateMarkers(visibleText, LOADING_STATE_PATTERNS);
|
|
496
555
|
const errorStateMarkers = stateMarkers(visibleText, ERROR_STATE_PATTERNS);
|
|
497
556
|
const emptyStateMarkers = stateMarkers(visibleText, EMPTY_STATE_PATTERNS);
|
|
557
|
+
const rows = Array.isArray(value?.rows) ? value.rows : [];
|
|
558
|
+
const textShowsEllipsis = rows.some((row) => row.cells.some((cell) => /\u2026|\.\.\./.test(cell.text)));
|
|
559
|
+
const truncationReasons = Array.isArray(value?.truncationReasons)
|
|
560
|
+
? value.truncationReasons.filter((reason): reason is string => typeof reason === "string")
|
|
561
|
+
: [];
|
|
562
|
+
if (textShowsEllipsis && !truncationReasons.includes("one or more cells are visibly clipped")) {
|
|
563
|
+
truncationReasons.push("one or more cells are visibly clipped");
|
|
564
|
+
}
|
|
498
565
|
return {
|
|
499
566
|
visibleText,
|
|
500
|
-
rows
|
|
567
|
+
rows,
|
|
568
|
+
truncated: value?.truncated === true || textShowsEllipsis,
|
|
569
|
+
truncationReasons,
|
|
501
570
|
loadingStateDetected: value?.loadingStateDetected === true || loadingStateMarkers.length > 0,
|
|
502
571
|
errorStateDetected: value?.errorStateDetected === true || errorStateMarkers.length > 0,
|
|
503
572
|
errorStateMarkers,
|
|
@@ -9,8 +9,8 @@ import type {
|
|
|
9
9
|
PaneTemplateDef,
|
|
10
10
|
} from "../../types/plugin";
|
|
11
11
|
|
|
12
|
-
export type PaneFunctionReadiness = "ready" | "partial" | "unsupported";
|
|
13
|
-
export type PaneFunctionScreenshotReadiness = PaneFunctionReadiness
|
|
12
|
+
export type PaneFunctionReadiness = "ready" | "partial" | "live-dom" | "unsupported";
|
|
13
|
+
export type PaneFunctionScreenshotReadiness = PaneFunctionReadiness;
|
|
14
14
|
export type PaneFunctionTickerCardinality = "none" | "one" | "one-or-more" | "two-or-more" | "one-or-two";
|
|
15
15
|
export type PaneFunctionOptionType = HeadlessPaneOptionType;
|
|
16
16
|
export type PaneFunctionOptionValue = HeadlessPaneOptionValue;
|
|
@@ -376,28 +376,80 @@ const CAPABILITIES: Record<string, PaneFunctionCapability> = {
|
|
|
376
376
|
intents: ["chart intraday security prices"],
|
|
377
377
|
outputKind: "price-history",
|
|
378
378
|
reportReadiness: "ready",
|
|
379
|
-
screenshotReadiness: "
|
|
379
|
+
screenshotReadiness: "ready",
|
|
380
380
|
dataRequirements: ["intraday price history"],
|
|
381
|
-
limitations: ["Intraday availability
|
|
382
|
-
options: [
|
|
381
|
+
limitations: ["Intraday availability and historical retention depend on the selected market-data provider."],
|
|
382
|
+
options: [
|
|
383
|
+
{
|
|
384
|
+
key: "rangePreset",
|
|
385
|
+
description: "Session window. 1D selects the latest session and 1W selects the latest five sessions.",
|
|
386
|
+
type: "enum",
|
|
387
|
+
aliases: ["range"],
|
|
388
|
+
values: RANGE_VALUES.filter(({ value }) => value === "1D" || value === "1W"),
|
|
389
|
+
defaultValue: "1D",
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
key: "chartResolution",
|
|
393
|
+
description: "Intraday sampling interval. Auto uses 1m for 1D and 5m for 1W.",
|
|
394
|
+
type: "enum",
|
|
395
|
+
aliases: ["resolution"],
|
|
396
|
+
values: ["auto", "5m", "15m", "1h"].map((value) => ({ value })),
|
|
397
|
+
defaultValue: "auto",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
key: "session",
|
|
401
|
+
description: "Specific exchange-local session in YYYY-MM-DD form. Overrides the session window.",
|
|
402
|
+
type: "string",
|
|
403
|
+
},
|
|
404
|
+
],
|
|
383
405
|
},
|
|
384
406
|
};
|
|
385
407
|
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
408
|
+
const NON_DATA_PANE_IDS = new Set([
|
|
409
|
+
"account-management",
|
|
410
|
+
"brokers",
|
|
411
|
+
"changelog",
|
|
412
|
+
"chat",
|
|
413
|
+
"connections",
|
|
414
|
+
"data-catalog",
|
|
415
|
+
"debug",
|
|
416
|
+
"help",
|
|
417
|
+
"ibkr-trading",
|
|
418
|
+
"layout-marketplace",
|
|
419
|
+
"local-agent-workspace",
|
|
420
|
+
"macro-tv",
|
|
421
|
+
"plugin-marketplace",
|
|
422
|
+
"world-venue-map",
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
const RENDERED_VIEW_LIMITATION =
|
|
426
|
+
"Reports contain only values exposed by the rendered view and may omit clipped or off-screen data.";
|
|
427
|
+
|
|
428
|
+
function fallbackCapability(
|
|
429
|
+
template: PaneTemplateDef | undefined,
|
|
430
|
+
pane: PaneDef,
|
|
431
|
+
): PaneFunctionCapability {
|
|
432
|
+
const dataPane = isDataPaneForDomFallback(pane);
|
|
433
|
+
return {
|
|
434
|
+
id: template?.id ?? pane.id,
|
|
435
|
+
botSafe: false,
|
|
436
|
+
tickerCardinality: "none",
|
|
437
|
+
aliases: [],
|
|
438
|
+
intents: [],
|
|
439
|
+
outputKind: dataPane ? "rendered-view" : "pane",
|
|
440
|
+
reportReadiness: dataPane ? "live-dom" : "unsupported",
|
|
441
|
+
screenshotReadiness: "live-dom",
|
|
442
|
+
dataRequirements: [],
|
|
443
|
+
limitations: [dataPane
|
|
444
|
+
? RENDERED_VIEW_LIMITATION
|
|
445
|
+
: "This pane is an interactive surface and does not expose a data report."],
|
|
446
|
+
options: [],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function isDataPaneForDomFallback(pane: Pick<PaneDef, "id">): boolean {
|
|
451
|
+
return !NON_DATA_PANE_IDS.has(pane.id);
|
|
452
|
+
}
|
|
401
453
|
|
|
402
454
|
function optionToken(value: string): string {
|
|
403
455
|
return value.trim().toLowerCase().replace(/[\s_.-]+/g, "");
|
|
@@ -431,13 +483,14 @@ export function getPaneFunctionCapability(
|
|
|
431
483
|
template: PaneTemplateDef | undefined,
|
|
432
484
|
pane: PaneDef,
|
|
433
485
|
): PaneFunctionCapability {
|
|
434
|
-
const
|
|
486
|
+
const fallback = fallbackCapability(template, pane);
|
|
487
|
+
const existing = template ? CAPABILITIES[template.id] ?? fallback : fallback;
|
|
435
488
|
const headless = getHeadlessPaneDefinition(template, pane);
|
|
436
489
|
if (!headless) return existing;
|
|
437
490
|
|
|
438
|
-
const hasExistingCapability = existing !==
|
|
491
|
+
const hasExistingCapability = existing !== fallback;
|
|
439
492
|
return {
|
|
440
|
-
...(hasExistingCapability ? existing :
|
|
493
|
+
...(hasExistingCapability ? existing : fallback),
|
|
441
494
|
id: hasExistingCapability ? existing.id : template?.id ?? pane.id,
|
|
442
495
|
botSafe: true,
|
|
443
496
|
tickerCardinality: headlessTickerCardinality(headless.argument),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { apiClient } from "../../api-client";
|
|
3
|
+
import type { MarketContext } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
resolvePersistedCloudSessionToken,
|
|
6
|
+
withPersistedCloudSession,
|
|
7
|
+
} from "./cloud-session";
|
|
8
|
+
|
|
9
|
+
function contextWithSession(
|
|
10
|
+
key: string,
|
|
11
|
+
sessionToken: string,
|
|
12
|
+
): Pick<MarketContext, "persistence"> {
|
|
13
|
+
return {
|
|
14
|
+
persistence: {
|
|
15
|
+
pluginState: {
|
|
16
|
+
get: (_pluginId: string, candidate: string) => candidate === key
|
|
17
|
+
? { value: { sessionToken }, schemaVersion: 1, updatedAt: 1 }
|
|
18
|
+
: null,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
} as Pick<MarketContext, "persistence">;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("pane function Cloud session", () => {
|
|
25
|
+
test("restores the current and legacy persisted session keys", () => {
|
|
26
|
+
expect(resolvePersistedCloudSessionToken(
|
|
27
|
+
contextWithSession("resume:session", "current-session"),
|
|
28
|
+
)).toBe("current-session");
|
|
29
|
+
expect(resolvePersistedCloudSessionToken(
|
|
30
|
+
contextWithSession("session", "legacy-session"),
|
|
31
|
+
)).toBe("legacy-session");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("scopes the persisted session to one report load", async () => {
|
|
35
|
+
const initial = apiClient.getSessionToken();
|
|
36
|
+
apiClient.setSessionToken("previous-session");
|
|
37
|
+
try {
|
|
38
|
+
let activeSession: string | null = null;
|
|
39
|
+
const result = await withPersistedCloudSession(
|
|
40
|
+
contextWithSession("resume:session", "report-session"),
|
|
41
|
+
async () => {
|
|
42
|
+
activeSession = apiClient.getSessionToken();
|
|
43
|
+
return 42;
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(result).toBe(42);
|
|
48
|
+
expect(activeSession).toBe("report-session");
|
|
49
|
+
expect(apiClient.getSessionToken()).toBe("previous-session");
|
|
50
|
+
} finally {
|
|
51
|
+
apiClient.setSessionToken(initial);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { apiClient } from "../../api-client";
|
|
2
|
+
import type { MarketContext } from "../types";
|
|
3
|
+
|
|
4
|
+
const CLOUD_PLUGIN_ID = "gloomberb-cloud";
|
|
5
|
+
const CLOUD_SESSION_KEYS = ["resume:session", "session"] as const;
|
|
6
|
+
|
|
7
|
+
interface PersistedCloudSession {
|
|
8
|
+
sessionToken?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolvePersistedCloudSessionToken(
|
|
12
|
+
context: Pick<MarketContext, "persistence">,
|
|
13
|
+
): string | null {
|
|
14
|
+
for (const key of CLOUD_SESSION_KEYS) {
|
|
15
|
+
const value = context.persistence.pluginState.get<PersistedCloudSession>(
|
|
16
|
+
CLOUD_PLUGIN_ID,
|
|
17
|
+
key,
|
|
18
|
+
1,
|
|
19
|
+
)?.value;
|
|
20
|
+
if (typeof value?.sessionToken === "string" && value.sessionToken.length > 0) {
|
|
21
|
+
return value.sessionToken;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function withPersistedCloudSession<T>(
|
|
28
|
+
context: Pick<MarketContext, "persistence">,
|
|
29
|
+
run: () => Promise<T>,
|
|
30
|
+
): Promise<T> {
|
|
31
|
+
const previousSessionToken = apiClient.getSessionToken();
|
|
32
|
+
apiClient.setSessionToken(resolvePersistedCloudSessionToken(context));
|
|
33
|
+
try {
|
|
34
|
+
return await run();
|
|
35
|
+
} finally {
|
|
36
|
+
apiClient.setSessionToken(previousSessionToken);
|
|
37
|
+
}
|
|
38
|
+
}
|