gloomberb 0.4.1 → 0.5.1
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 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/pane-sizing.ts +10 -0
- package/src/components/layout/pane.tsx +9 -2
- package/src/components/layout/shell.test.tsx +282 -28
- package/src/components/layout/shell.tsx +10 -7
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +83 -18
- package/src/plugins/builtin/chat-controller.ts +26 -14
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +424 -120
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/api-client.test.ts +90 -0
- package/src/utils/api-client.ts +56 -16
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -363,6 +363,53 @@ describe("prediction markets plugin registration and services", () => {
|
|
|
363
363
|
expect(markets[0]?.marketLabel).toBe("April 30");
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
test("keeps Polymarket catalog results when one page connection resets", async () => {
|
|
367
|
+
let resetFetchCount = 0;
|
|
368
|
+
|
|
369
|
+
globalThis.fetch = (async (input: Request | string | URL) => {
|
|
370
|
+
const url = String(input);
|
|
371
|
+
if (url.includes("gamma-api.polymarket.com/events?")) {
|
|
372
|
+
if (url.includes("offset=200")) {
|
|
373
|
+
resetFetchCount += 1;
|
|
374
|
+
throw Object.assign(
|
|
375
|
+
new Error("The socket connection was closed unexpectedly."),
|
|
376
|
+
{ code: "ECONNRESET" },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return new Response(
|
|
380
|
+
JSON.stringify([
|
|
381
|
+
{
|
|
382
|
+
id: "event-stable",
|
|
383
|
+
title: "Stable catalog page",
|
|
384
|
+
tags: [{ label: "Macro", slug: "economy" }],
|
|
385
|
+
markets: [
|
|
386
|
+
{
|
|
387
|
+
id: "pm-stable",
|
|
388
|
+
question: "Will the stable page load?",
|
|
389
|
+
conditionId: "cond-stable",
|
|
390
|
+
outcomes: '["Yes","No"]',
|
|
391
|
+
outcomePrices: '["0.57","0.43"]',
|
|
392
|
+
clobTokenIds: '["yes-stable","no-stable"]',
|
|
393
|
+
volume24hr: 125000,
|
|
394
|
+
active: true,
|
|
395
|
+
closed: false,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
]),
|
|
400
|
+
{ status: 200 },
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
404
|
+
}) as unknown as typeof fetch;
|
|
405
|
+
|
|
406
|
+
const markets = await loadPolymarketCatalog("", "all");
|
|
407
|
+
|
|
408
|
+
expect(markets).toHaveLength(1);
|
|
409
|
+
expect(markets[0]?.marketId).toBe("pm-stable");
|
|
410
|
+
expect(resetFetchCount).toBe(3);
|
|
411
|
+
});
|
|
412
|
+
|
|
366
413
|
test("uses remote catalog endpoints for search and category changes", async () => {
|
|
367
414
|
const { fetchUrls } = installPredictionMarketMocks();
|
|
368
415
|
|
|
@@ -420,7 +467,7 @@ describe("prediction markets plugin registration and services", () => {
|
|
|
420
467
|
}
|
|
421
468
|
if (url.includes("gamma-api.polymarket.com/events/event-1")) {
|
|
422
469
|
eventFetchCount += 1;
|
|
423
|
-
if (eventFetchCount
|
|
470
|
+
if (eventFetchCount <= 3) {
|
|
424
471
|
return new Response("{}", { status: 500 });
|
|
425
472
|
}
|
|
426
473
|
return new Response(
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
2
|
+
import { createThrottledFetch } from "../../../utils/throttled-fetch";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_SOURCE_KEY = "remote";
|
|
5
|
+
const PREDICTION_FETCH = createThrottledFetch({
|
|
6
|
+
requestsPerMinute: 120,
|
|
7
|
+
maxRetries: 2,
|
|
8
|
+
timeoutMs: 10_000,
|
|
9
|
+
backoffBaseMs: 250,
|
|
10
|
+
dedupeGetRequests: false,
|
|
11
|
+
defaultHeaders: {
|
|
12
|
+
Accept: "application/json",
|
|
13
|
+
"User-Agent": "gloomberb-prediction-markets",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
4
16
|
|
|
5
17
|
export const PREDICTION_CACHE_POLICIES = {
|
|
6
18
|
catalog: { staleMs: 30_000, expireMs: 10 * 60_000 },
|
|
@@ -28,13 +40,7 @@ export function getPredictionMarketsPersistence(): PluginPersistence | null {
|
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
export async function fetchJson<T>(url: string): Promise<T> {
|
|
31
|
-
const response = await fetch(url
|
|
32
|
-
signal: AbortSignal.timeout(10_000),
|
|
33
|
-
headers: {
|
|
34
|
-
Accept: "application/json",
|
|
35
|
-
"User-Agent": "gloomberb-prediction-markets",
|
|
36
|
-
},
|
|
37
|
-
});
|
|
43
|
+
const response = await PREDICTION_FETCH.fetch(url);
|
|
38
44
|
if (!response.ok) {
|
|
39
45
|
throw new Error(`Request failed (${response.status}) for ${url}`);
|
|
40
46
|
}
|
|
@@ -219,6 +219,27 @@ function buildPolymarketSearchUrl(query: string): string {
|
|
|
219
219
|
return url.toString();
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
async function loadPolymarketCatalogPages(
|
|
223
|
+
offsets: number[],
|
|
224
|
+
tagSlug?: string,
|
|
225
|
+
): Promise<PolymarketEventRecord[]> {
|
|
226
|
+
const results = await Promise.allSettled(
|
|
227
|
+
offsets.map((offset) =>
|
|
228
|
+
fetchJson<PolymarketEventRecord[]>(
|
|
229
|
+
buildPolymarketCatalogUrl(offset, tagSlug),
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
const pages = results.flatMap((result) =>
|
|
234
|
+
result.status === "fulfilled" ? result.value : [],
|
|
235
|
+
);
|
|
236
|
+
if (pages.length > 0) return pages;
|
|
237
|
+
|
|
238
|
+
const rejected = results.find((result) => result.status === "rejected");
|
|
239
|
+
if (rejected?.status === "rejected") throw rejected.reason;
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
export function normalizePolymarketMarket(
|
|
223
244
|
record: PolymarketMarketRecord,
|
|
224
245
|
options?: {
|
|
@@ -360,12 +381,11 @@ export async function loadPolymarketCatalog(
|
|
|
360
381
|
if (categoryId !== "all") {
|
|
361
382
|
const tagSlugs = getPolymarketCategoryTagSlugs(categoryId);
|
|
362
383
|
const categoryPages = await Promise.all(
|
|
363
|
-
tagSlugs.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
),
|
|
384
|
+
tagSlugs.map((tagSlug) =>
|
|
385
|
+
loadPolymarketCatalogPages(
|
|
386
|
+
POLYMARKET_CATEGORY_OFFSETS,
|
|
387
|
+
tagSlug,
|
|
388
|
+
).catch(() => []),
|
|
369
389
|
),
|
|
370
390
|
);
|
|
371
391
|
const categorized = sortPolymarketMarkets(
|
|
@@ -374,13 +394,11 @@ export async function loadPolymarketCatalog(
|
|
|
374
394
|
if (categorized.length > 0) return categorized;
|
|
375
395
|
}
|
|
376
396
|
|
|
377
|
-
const pages = await
|
|
378
|
-
POLYMARKET_CATALOG_OFFSETS
|
|
379
|
-
fetchJson<PolymarketEventRecord[]>(buildPolymarketCatalogUrl(offset)),
|
|
380
|
-
),
|
|
397
|
+
const pages = await loadPolymarketCatalogPages(
|
|
398
|
+
POLYMARKET_CATALOG_OFFSETS,
|
|
381
399
|
);
|
|
382
400
|
return sortPolymarketMarkets(
|
|
383
|
-
flattenPolymarketEvents(pages
|
|
401
|
+
flattenPolymarketEvents(pages, "", categoryId),
|
|
384
402
|
);
|
|
385
403
|
},
|
|
386
404
|
PREDICTION_CACHE_POLICIES.catalog,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PredictionBookLevel, PredictionTrade } from "../types";
|
|
2
2
|
import { parseFloatSafe } from "./fetch";
|
|
3
3
|
|
|
4
|
+
const POLYMARKET_HEARTBEAT_MS = 10_000;
|
|
5
|
+
|
|
4
6
|
interface PolymarketLiveCallbacks {
|
|
5
7
|
onBestBidAsk?: (
|
|
6
8
|
assetId: string,
|
|
@@ -38,8 +40,23 @@ export function subscribePolymarketMarket(
|
|
|
38
40
|
let closed = false;
|
|
39
41
|
let socket: WebSocket | null = null;
|
|
40
42
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
|
|
45
|
+
const clearHeartbeat = () => {
|
|
46
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
47
|
+
heartbeatTimer = null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const sendHeartbeat = () => {
|
|
51
|
+
try {
|
|
52
|
+
socket?.send("PING");
|
|
53
|
+
} catch {
|
|
54
|
+
// The close handler will schedule reconnects for broken sockets.
|
|
55
|
+
}
|
|
56
|
+
};
|
|
41
57
|
|
|
42
58
|
const connect = () => {
|
|
59
|
+
clearHeartbeat();
|
|
43
60
|
socket = new WebSocket(
|
|
44
61
|
"wss://ws-subscriptions-clob.polymarket.com/ws/market",
|
|
45
62
|
);
|
|
@@ -51,6 +68,7 @@ export function subscribePolymarketMarket(
|
|
|
51
68
|
custom_feature_enabled: true,
|
|
52
69
|
}),
|
|
53
70
|
);
|
|
71
|
+
heartbeatTimer = setInterval(sendHeartbeat, POLYMARKET_HEARTBEAT_MS);
|
|
54
72
|
});
|
|
55
73
|
socket.addEventListener("message", (event) => {
|
|
56
74
|
try {
|
|
@@ -89,6 +107,7 @@ export function subscribePolymarketMarket(
|
|
|
89
107
|
}
|
|
90
108
|
});
|
|
91
109
|
socket.addEventListener("close", () => {
|
|
110
|
+
clearHeartbeat();
|
|
92
111
|
socket = null;
|
|
93
112
|
if (closed) return;
|
|
94
113
|
reconnectTimer = setTimeout(connect, 1_500);
|
|
@@ -103,6 +122,7 @@ export function subscribePolymarketMarket(
|
|
|
103
122
|
return () => {
|
|
104
123
|
closed = true;
|
|
105
124
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
125
|
+
clearHeartbeat();
|
|
106
126
|
socket?.close();
|
|
107
127
|
};
|
|
108
128
|
}
|
|
@@ -137,6 +137,8 @@ export function createRuntime(): PluginRuntimeAccess {
|
|
|
137
137
|
const listeners = new Map<string, Set<() => void>>();
|
|
138
138
|
|
|
139
139
|
return {
|
|
140
|
+
pinTicker() {},
|
|
141
|
+
navigateTicker() {},
|
|
140
142
|
subscribeResumeState(pluginId, key, listener) {
|
|
141
143
|
const listenerKey = `${pluginId}:${key}`;
|
|
142
144
|
if (!listeners.has(listenerKey)) listeners.set(listenerKey, new Set());
|
package/src/plugins/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { BrokerInstanceConfig, LayoutConfig, PaneInstanceConfig } from "../
|
|
|
8
8
|
import type { DataProvider } from "../types/data-provider";
|
|
9
9
|
import type { TickerFinancials } from "../types/financials";
|
|
10
10
|
import type {
|
|
11
|
+
AppNotificationRequest,
|
|
11
12
|
CommandDef,
|
|
12
13
|
CustomColumnDef,
|
|
13
14
|
DetailTabDef,
|
|
@@ -27,7 +28,7 @@ import type {
|
|
|
27
28
|
import type { TickerRecord } from "../types/ticker";
|
|
28
29
|
import { addPaneFloating, removePane } from "./pane-manager";
|
|
29
30
|
import { EventBus, type PluginEvents } from "./event-bus";
|
|
30
|
-
import { createPaneInstance } from "../types/config";
|
|
31
|
+
import { createPaneInstance, resolvePaneInstance } from "../types/config";
|
|
31
32
|
import { createPluginPersistence } from "./plugin-persistence";
|
|
32
33
|
import { debugLog } from "../utils/debug-log";
|
|
33
34
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "./plugin-runtime";
|
|
@@ -104,6 +105,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
104
105
|
switchPanelFn: ((panel: "left" | "right") => void) = () => {};
|
|
105
106
|
switchTabFn: ((tabId: string, paneId?: string) => void) = () => {};
|
|
106
107
|
openCommandBarFn: ((query?: string) => void) = () => {};
|
|
108
|
+
openPluginCommandWorkflowFn: ((commandId: string) => void) = () => {};
|
|
107
109
|
openPaneSettingsFn: ((paneId?: string) => void) = () => {};
|
|
108
110
|
showPaneFn: ((paneId: string) => void) = () => {};
|
|
109
111
|
createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
|
|
@@ -111,12 +113,22 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
111
113
|
hidePaneFn: ((paneId: string) => void) = () => {};
|
|
112
114
|
focusPaneFn: ((paneId: string) => void) = () => {};
|
|
113
115
|
pinTickerFn: ((symbol: string, options?: { floating?: boolean; paneType?: string }) => void) = () => {};
|
|
116
|
+
navigateTickerFn: ((symbol: string) => void) = () => {};
|
|
117
|
+
pinTicker = (symbol: string, options?: { floating?: boolean; paneType?: string }) => {
|
|
118
|
+
this.pinTickerFn(symbol, options);
|
|
119
|
+
};
|
|
120
|
+
navigateTicker = (symbol: string) => {
|
|
121
|
+
this.navigateTickerFn(symbol);
|
|
122
|
+
};
|
|
123
|
+
openPluginCommandWorkflow = (commandId: string) => {
|
|
124
|
+
this.openPluginCommandWorkflowFn(commandId);
|
|
125
|
+
};
|
|
114
126
|
|
|
115
127
|
getLayoutFn: (() => LayoutConfig) = () => ({ dockRoot: null, instances: [], floating: [] });
|
|
116
128
|
updateLayoutFn: ((layout: LayoutConfig) => void) = () => {};
|
|
117
129
|
getTermSizeFn: (() => { width: number; height: number }) = () => ({ width: 120, height: 40 });
|
|
118
130
|
|
|
119
|
-
|
|
131
|
+
notifyFn: ((notification: AppNotificationRequest) => void) = () => {};
|
|
120
132
|
getPaneRuntimeStateFn: ((paneId: string) => PaneRuntimeState | null) = () => null;
|
|
121
133
|
updatePaneRuntimeStateFn: ((paneId: string, patch: Partial<PaneRuntimeState>) => void) = () => {};
|
|
122
134
|
applyPaneSettingValueFn: ((paneId: string, field: import("../types/plugin").PaneSettingField, value: unknown) => Promise<void>) = async () => {};
|
|
@@ -177,6 +189,10 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
177
189
|
return this.pluginItems.get(pluginId)?.paneTemplates ?? [];
|
|
178
190
|
}
|
|
179
191
|
|
|
192
|
+
notify(notification: AppNotificationRequest): void {
|
|
193
|
+
this.notifyFn(notification);
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
private getOrCreatePluginItems(pluginId: string): PluginItems {
|
|
181
197
|
const existing = this.pluginItems.get(pluginId);
|
|
182
198
|
if (existing) return existing;
|
|
@@ -272,16 +288,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
272
288
|
return Object.keys(this.getConfigFn().pluginConfig[pluginId] ?? {}).sort();
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
private resolvePrimaryPaneInstanceId(paneId: string): string | undefined {
|
|
276
|
-
const layout = this.getLayoutFn();
|
|
277
|
-
return layout.instances.find((instance) => instance.paneId === paneId)?.instanceId;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
291
|
private resolvePaneTarget(paneId: string): string | undefined {
|
|
281
|
-
|
|
282
|
-
const isInstanceId = layout.instances.some((instance) => instance.instanceId === paneId);
|
|
283
|
-
if (isInstanceId) return paneId;
|
|
284
|
-
return this.resolvePrimaryPaneInstanceId(paneId);
|
|
292
|
+
return resolvePaneInstance(this.getLayoutFn(), paneId)?.instanceId;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
resolvePaneSettings(paneId: string): {
|
|
@@ -514,7 +522,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
514
522
|
createPaneFromTemplate: (templateId, options) => this.createPaneFromTemplateFn(templateId, options),
|
|
515
523
|
hidePane: (paneId) => this.hidePaneFn(paneId),
|
|
516
524
|
focusPane: (paneId) => this.focusPaneFn(paneId),
|
|
517
|
-
pinTicker:
|
|
525
|
+
pinTicker: this.pinTicker,
|
|
526
|
+
navigateTicker: this.navigateTicker,
|
|
518
527
|
openPaneSettings: (paneId) => this.openPaneSettingsFn(paneId),
|
|
519
528
|
|
|
520
529
|
on: <K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void) => {
|
|
@@ -526,7 +535,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
526
535
|
|
|
527
536
|
showWidget: (widgetId) => this.showWidget(widgetId),
|
|
528
537
|
hideWidget: (widgetId) => this.hideWidget(widgetId),
|
|
529
|
-
|
|
538
|
+
notify: (notification) => this.notifyFn(notification),
|
|
530
539
|
};
|
|
531
540
|
}
|
|
532
541
|
|
|
@@ -1293,7 +1293,8 @@ describe("ProviderRouter", () => {
|
|
|
1293
1293
|
test("drops stale cached cloud quotes while preserving cached cloud fundamentals", () => {
|
|
1294
1294
|
const dbPath = createTempDbPath("stale-cloud-cached-financials");
|
|
1295
1295
|
const persistence = new AppPersistence(dbPath);
|
|
1296
|
-
const now = Date.
|
|
1296
|
+
const now = Date.now();
|
|
1297
|
+
const previousSession = now - 24 * 60 * 60_000;
|
|
1297
1298
|
|
|
1298
1299
|
persistence.resources.set(
|
|
1299
1300
|
{
|
|
@@ -1313,7 +1314,7 @@ describe("ProviderRouter", () => {
|
|
|
1313
1314
|
currency: "EUR",
|
|
1314
1315
|
change: 28,
|
|
1315
1316
|
changePercent: 5.6,
|
|
1316
|
-
lastUpdated:
|
|
1317
|
+
lastUpdated: previousSession,
|
|
1317
1318
|
marketState: "REGULAR",
|
|
1318
1319
|
exchangeName: "FWB2",
|
|
1319
1320
|
listingExchangeName: "FWB2",
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -507,9 +507,21 @@ export class SecEdgarClient {
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
async getFilingContent(filing: Pick<SecFilingItem, "primaryDocumentUrl" | "filingUrl" | "form">): Promise<string | null> {
|
|
510
|
-
|
|
510
|
+
let targetUrl = filing.primaryDocumentUrl || filing.filingUrl;
|
|
511
511
|
if (!targetUrl) return null;
|
|
512
512
|
|
|
513
|
+
// SEC ownership forms (3/4/5) have XSL-prefixed primaryDocument paths
|
|
514
|
+
// (e.g., "xslF345X06/wk-form4_xxx.xml") which return rendered HTML.
|
|
515
|
+
// Strip the prefix and return the raw XML for programmatic parsing.
|
|
516
|
+
const ownershipForms = new Set(["3", "4", "5"]);
|
|
517
|
+
if (filing.primaryDocumentUrl && filing.form && ownershipForms.has(filing.form.trim())) {
|
|
518
|
+
const rawUrl = filing.primaryDocumentUrl.replace(/\/xsl[^/]+\//, "/");
|
|
519
|
+
if (rawUrl !== filing.primaryDocumentUrl) {
|
|
520
|
+
const { body } = await this.fetchText(rawUrl);
|
|
521
|
+
return body;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
513
525
|
if (filing.primaryDocumentUrl && isPdfDocument("", "", filing.primaryDocumentUrl)) {
|
|
514
526
|
const alternativeUrl = filing.filingUrl
|
|
515
527
|
? await this.findAlternativeHtmlDocumentUrl(filing.filingUrl, filing.primaryDocumentUrl)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Quote, Fundamentals, FinancialStatement, PricePoint, TickerFinancials, MarketState, OptionContract, OptionsChain, CompanyProfile } from "../types/financials";
|
|
2
|
-
import type { DataProvider, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
|
|
2
|
+
import type { DataProvider, EarningsEvent, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
|
|
3
3
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
4
4
|
import {
|
|
5
5
|
normalizeChartResolutionSupport,
|
|
@@ -1065,4 +1065,78 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
throw lastError || new Error(`No options chain for ${ticker}`);
|
|
1067
1067
|
}
|
|
1068
|
+
|
|
1069
|
+
async getEarningsCalendar(symbols: string[], _context?: MarketDataRequestContext): Promise<EarningsEvent[]> {
|
|
1070
|
+
const results: EarningsEvent[] = [];
|
|
1071
|
+
const BATCH_SIZE = 5;
|
|
1072
|
+
|
|
1073
|
+
for (let i = 0; i < symbols.length; i += BATCH_SIZE) {
|
|
1074
|
+
if (i > 0) await new Promise((r) => setTimeout(r, 200));
|
|
1075
|
+
const batch = symbols.slice(i, i + BATCH_SIZE);
|
|
1076
|
+
|
|
1077
|
+
const settled = await Promise.allSettled(
|
|
1078
|
+
batch.map(async (symbol) => {
|
|
1079
|
+
const params = new URLSearchParams({ modules: "calendarEvents,earningsTrend,quoteType" });
|
|
1080
|
+
const url = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?${params}`;
|
|
1081
|
+
const data = await this.fetchJsonWithCrumb<{
|
|
1082
|
+
quoteSummary?: {
|
|
1083
|
+
result?: Array<{
|
|
1084
|
+
calendarEvents?: {
|
|
1085
|
+
earnings?: {
|
|
1086
|
+
earningsDate?: Array<{ raw: number }>;
|
|
1087
|
+
earningsAverage?: { raw: number };
|
|
1088
|
+
revenueAverage?: { raw: number };
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
earningsTrend?: {
|
|
1092
|
+
trend?: Array<{
|
|
1093
|
+
period: string;
|
|
1094
|
+
earningsEstimate?: { avg?: { raw: number } };
|
|
1095
|
+
revenueEstimate?: { avg?: { raw: number } };
|
|
1096
|
+
}>;
|
|
1097
|
+
};
|
|
1098
|
+
quoteType?: {
|
|
1099
|
+
shortName?: string;
|
|
1100
|
+
longName?: string;
|
|
1101
|
+
};
|
|
1102
|
+
}>;
|
|
1103
|
+
};
|
|
1104
|
+
}>(`earnings ${symbol}`, url);
|
|
1105
|
+
|
|
1106
|
+
const mod = data.quoteSummary?.result?.[0];
|
|
1107
|
+
if (!mod) return null;
|
|
1108
|
+
|
|
1109
|
+
const cal = mod.calendarEvents?.earnings;
|
|
1110
|
+
if (!cal?.earningsDate?.length) return null;
|
|
1111
|
+
|
|
1112
|
+
const earningsDate = new Date(cal.earningsDate[0]!.raw * 1000);
|
|
1113
|
+
if (isNaN(earningsDate.getTime())) return null;
|
|
1114
|
+
|
|
1115
|
+
const currentQtr = mod.earningsTrend?.trend?.find((t) => t.period === "0q");
|
|
1116
|
+
const name = mod.quoteType?.shortName || mod.quoteType?.longName || symbol;
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
symbol,
|
|
1120
|
+
name,
|
|
1121
|
+
earningsDate,
|
|
1122
|
+
epsEstimate: currentQtr?.earningsEstimate?.avg?.raw ?? cal.earningsAverage?.raw ?? null,
|
|
1123
|
+
epsActual: null,
|
|
1124
|
+
revenueEstimate: currentQtr?.revenueEstimate?.avg?.raw ?? cal.revenueAverage?.raw ?? null,
|
|
1125
|
+
revenueActual: null,
|
|
1126
|
+
surprise: null,
|
|
1127
|
+
timing: "" as const,
|
|
1128
|
+
};
|
|
1129
|
+
}),
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
for (const result of settled) {
|
|
1133
|
+
if (result.status === "fulfilled" && result.value) {
|
|
1134
|
+
results.push(result.value);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
results.sort((a, b) => a.earningsDate.getTime() - b.earningsDate.getTime());
|
|
1140
|
+
return results;
|
|
1141
|
+
}
|
|
1068
1142
|
}
|
|
@@ -3,7 +3,7 @@ import { act, useRef, type Dispatch } from "react";
|
|
|
3
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
4
4
|
import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneTicker, type AppAction } from "./app-context";
|
|
5
5
|
import { cloneLayout, createDefaultConfig, type AppConfig } from "../types/config";
|
|
6
|
-
import { getCurrentThemeId } from "../theme/colors";
|
|
6
|
+
import { applyTheme, getCurrentThemeId } from "../theme/colors";
|
|
7
7
|
|
|
8
8
|
const TEST_PANE_ID = "ticker-detail:test";
|
|
9
9
|
|
|
@@ -51,6 +51,7 @@ describe("pane selectors", () => {
|
|
|
51
51
|
testSetup?.renderer.destroy();
|
|
52
52
|
testSetup = undefined;
|
|
53
53
|
capturedDispatch = null;
|
|
54
|
+
applyTheme("amber");
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
test("does not rerender usePaneTicker consumers for unrelated app state updates", async () => {
|
|
@@ -96,4 +97,19 @@ describe("pane selectors", () => {
|
|
|
96
97
|
await testSetup.renderOnce();
|
|
97
98
|
expect(testSetup.captureCharFrame()).toContain(`${TEST_PANE_ID}:green`);
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
test("uses the configured theme on the first provider render", async () => {
|
|
102
|
+
const config = createTickerDetailConfig("AAPL");
|
|
103
|
+
config.theme = "green";
|
|
104
|
+
|
|
105
|
+
testSetup = await testRender(
|
|
106
|
+
<AppProvider config={config}>
|
|
107
|
+
<ThemeSelectorHarness />
|
|
108
|
+
</AppProvider>,
|
|
109
|
+
{ width: 32, height: 4 },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await testSetup.renderOnce();
|
|
113
|
+
expect(testSetup.captureCharFrame()).toContain(`${TEST_PANE_ID}:green`);
|
|
114
|
+
});
|
|
99
115
|
});
|
|
@@ -22,6 +22,30 @@ describe("appReducer command bar state", () => {
|
|
|
22
22
|
expect(next.commandBarQuery).toBe("");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("stores and clears command bar launch requests", () => {
|
|
26
|
+
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
27
|
+
const opened = appReducer(initial, {
|
|
28
|
+
type: "SET_COMMAND_BAR",
|
|
29
|
+
open: true,
|
|
30
|
+
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
31
|
+
});
|
|
32
|
+
const relaunched = appReducer(opened, {
|
|
33
|
+
type: "SET_COMMAND_BAR",
|
|
34
|
+
open: true,
|
|
35
|
+
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
36
|
+
});
|
|
37
|
+
const closed = appReducer(relaunched, { type: "SET_COMMAND_BAR", open: false });
|
|
38
|
+
|
|
39
|
+
expect(opened.commandBarOpen).toBe(true);
|
|
40
|
+
expect(opened.commandBarLaunchRequest).toEqual({
|
|
41
|
+
kind: "plugin-command",
|
|
42
|
+
commandId: "set-alert",
|
|
43
|
+
sequence: 1,
|
|
44
|
+
});
|
|
45
|
+
expect(relaunched.commandBarLaunchRequest?.sequence).toBe(2);
|
|
46
|
+
expect(closed.commandBarLaunchRequest).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
25
49
|
test("toggle opens blank and clears on close", () => {
|
|
26
50
|
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
27
51
|
const opened = appReducer(initial, { type: "TOGGLE_COMMAND_BAR" });
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import type { SessionStore } from "../data/session-store";
|
|
15
15
|
import { saveConfig } from "../data/config-store";
|
|
16
|
-
import { applyTheme } from "../theme/colors";
|
|
16
|
+
import { applyTheme, syncTheme } from "../theme/colors";
|
|
17
17
|
import { isBrokerPortfolioId } from "../utils/broker-instances";
|
|
18
18
|
import { resolveTickerFinancialsQuoteState } from "../utils/quote-resolution";
|
|
19
19
|
import {
|
|
@@ -61,6 +61,12 @@ export interface CollectionSortPreference {
|
|
|
61
61
|
direction: SortDirection;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface CommandBarLaunchRequest {
|
|
65
|
+
kind: "plugin-command";
|
|
66
|
+
commandId: string;
|
|
67
|
+
sequence: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
export interface AppState {
|
|
65
71
|
config: AppConfig;
|
|
66
72
|
tickers: Map<string, TickerRecord>;
|
|
@@ -73,6 +79,7 @@ export interface AppState {
|
|
|
73
79
|
recentTickers: string[];
|
|
74
80
|
commandBarOpen: boolean;
|
|
75
81
|
commandBarQuery: string;
|
|
82
|
+
commandBarLaunchRequest: CommandBarLaunchRequest | null;
|
|
76
83
|
refreshing: Set<string>;
|
|
77
84
|
initialized: boolean;
|
|
78
85
|
statusBarVisible: boolean;
|
|
@@ -97,7 +104,12 @@ export type AppAction =
|
|
|
97
104
|
| { type: "TRACK_TICKER"; symbol: string | null }
|
|
98
105
|
| { type: "SET_ACTIVE_PANEL"; panel: "left" | "right" }
|
|
99
106
|
| { type: "TOGGLE_COMMAND_BAR" }
|
|
100
|
-
| {
|
|
107
|
+
| {
|
|
108
|
+
type: "SET_COMMAND_BAR";
|
|
109
|
+
open: boolean;
|
|
110
|
+
query?: string;
|
|
111
|
+
launch?: { kind: "plugin-command"; commandId: string } | null;
|
|
112
|
+
}
|
|
101
113
|
| { type: "SET_COMMAND_BAR_QUERY"; query: string }
|
|
102
114
|
| { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
|
|
103
115
|
| { type: "SET_BROKER_ACCOUNTS"; instanceId: string; accounts: BrokerAccount[] }
|
|
@@ -492,15 +504,23 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
492
504
|
|
|
493
505
|
case "TOGGLE_COMMAND_BAR":
|
|
494
506
|
return state.commandBarOpen
|
|
495
|
-
? { ...state, commandBarOpen: false, commandBarQuery: "" }
|
|
496
|
-
: { ...state, commandBarOpen: true, commandBarQuery: "" };
|
|
497
|
-
|
|
498
|
-
case "SET_COMMAND_BAR":
|
|
507
|
+
? { ...state, commandBarOpen: false, commandBarQuery: "", commandBarLaunchRequest: null }
|
|
508
|
+
: { ...state, commandBarOpen: true, commandBarQuery: "", commandBarLaunchRequest: null };
|
|
509
|
+
|
|
510
|
+
case "SET_COMMAND_BAR": {
|
|
511
|
+
const launchRequest = action.open && action.launch
|
|
512
|
+
? {
|
|
513
|
+
...action.launch,
|
|
514
|
+
sequence: (state.commandBarLaunchRequest?.sequence ?? 0) + 1,
|
|
515
|
+
}
|
|
516
|
+
: null;
|
|
499
517
|
return {
|
|
500
518
|
...state,
|
|
501
519
|
commandBarOpen: action.open,
|
|
502
520
|
commandBarQuery: action.open ? (action.query ?? "") : "",
|
|
521
|
+
commandBarLaunchRequest: launchRequest,
|
|
503
522
|
};
|
|
523
|
+
}
|
|
504
524
|
|
|
505
525
|
case "SET_COMMAND_BAR_QUERY":
|
|
506
526
|
return { ...state, commandBarQuery: action.query };
|
|
@@ -1018,6 +1038,7 @@ export function createInitialState(config: AppConfig, sessionSnapshot: AppSessio
|
|
|
1018
1038
|
recentTickers: config.recentTickers,
|
|
1019
1039
|
commandBarOpen: false,
|
|
1020
1040
|
commandBarQuery: "",
|
|
1041
|
+
commandBarLaunchRequest: null,
|
|
1021
1042
|
refreshing: new Set(),
|
|
1022
1043
|
initialized: false,
|
|
1023
1044
|
statusBarVisible: sessionSnapshot?.statusBarVisible !== false,
|
|
@@ -1043,6 +1064,8 @@ export function AppProvider({
|
|
|
1043
1064
|
sessionStore?: SessionStore;
|
|
1044
1065
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
1045
1066
|
}) {
|
|
1067
|
+
// Sync the configured theme before descendants read the shared palette on first render.
|
|
1068
|
+
syncTheme(config.theme);
|
|
1046
1069
|
const [state, dispatch] = useReducer(appReducer, { config, sessionSnapshot }, ({ config, sessionSnapshot: initialSessionSnapshot }) => (
|
|
1047
1070
|
createInitialState(config, initialSessionSnapshot)
|
|
1048
1071
|
));
|
package/src/theme/colors.ts
CHANGED
|
@@ -18,6 +18,11 @@ export function applyTheme(id: string): void {
|
|
|
18
18
|
Object.assign(colors, theme);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function syncTheme(id: string): void {
|
|
22
|
+
if (currentThemeId === id) return;
|
|
23
|
+
applyTheme(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export type ColorKey = keyof typeof colors;
|
|
22
27
|
|
|
23
28
|
export { blendHex } from "./color-utils";
|