gloomberb 0.4.0 → 0.5.0
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 +184 -69
- 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 +296 -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 -1
- package/src/components/layout/header.tsx +5 -4
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +263 -32
- package/src/components/layout/shell.tsx +10 -6
- 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 +62 -2
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +82 -5
- 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 +92 -18
- package/src/plugins/builtin/chat-controller.ts +40 -11
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +420 -119
- 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 +33 -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/gateway-service.test.ts +22 -0
- package/src/plugins/ibkr/gateway-service.ts +6 -0
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +8 -2
- 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 +129 -14
- package/src/plugins/prediction-markets/controller.ts +1 -1
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/index.tsx +5 -1
- package/src/plugins/prediction-markets/pane.test.tsx +78 -0
- package/src/plugins/prediction-markets/pane.tsx +7 -3
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +17 -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 +22 -5
- package/src/plugins/registry.ts +45 -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/updater.test.ts +2 -0
- package/src/utils/api-client.ts +12 -0
- 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
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { useAppState, usePaneInstanceId, type PaneRuntimeState } from "../state/app-context";
|
|
11
11
|
|
|
12
12
|
export interface PluginRuntimeAccess {
|
|
13
|
+
pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
|
|
14
|
+
navigateTicker(symbol: string): void;
|
|
13
15
|
subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
|
|
14
16
|
getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null;
|
|
15
17
|
setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void;
|
|
@@ -51,6 +53,14 @@ function usePluginRenderContext(): PluginRenderContextValue {
|
|
|
51
53
|
return context;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export function usePluginTickerActions() {
|
|
57
|
+
const { runtime } = usePluginRenderContext();
|
|
58
|
+
return {
|
|
59
|
+
pinTicker: runtime.pinTicker,
|
|
60
|
+
navigateTicker: runtime.navigateTicker,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export function getPluginPaneStateValue<T>(
|
|
55
65
|
paneState: PaneRuntimeState | undefined,
|
|
56
66
|
pluginId: string,
|
|
@@ -26,6 +26,7 @@ import { subscribePolymarketMarket } from "./services/polymarket-ws";
|
|
|
26
26
|
import type {
|
|
27
27
|
PredictionBrowseTab,
|
|
28
28
|
PredictionCategoryId,
|
|
29
|
+
PredictionVenue,
|
|
29
30
|
PredictionListRow,
|
|
30
31
|
PredictionMarketDetail,
|
|
31
32
|
PredictionMarketSummary,
|
|
@@ -36,6 +37,56 @@ import type {
|
|
|
36
37
|
|
|
37
38
|
const KEYBOARD_DETAIL_LOAD_DELAY_MS = 140;
|
|
38
39
|
|
|
40
|
+
function formatPredictionVenueLabel(venue: PredictionVenue): string {
|
|
41
|
+
return venue === "polymarket" ? "Polymarket" : "Kalshi";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function joinPredictionVenueLabels(venues: PredictionVenue[]): string {
|
|
45
|
+
const labels = [...new Set(venues.map(formatPredictionVenueLabel))];
|
|
46
|
+
if (labels.length <= 1) return labels[0] ?? "";
|
|
47
|
+
return `${labels.slice(0, -1).join(" and ")} and ${labels.at(-1)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatPredictionLoadError(
|
|
51
|
+
venue: PredictionVenue,
|
|
52
|
+
subject: "markets" | "market detail",
|
|
53
|
+
error: unknown,
|
|
54
|
+
): string {
|
|
55
|
+
const venueLabel = formatPredictionVenueLabel(venue);
|
|
56
|
+
const fallback = `Could not load ${venueLabel} ${subject}.`;
|
|
57
|
+
if (!(error instanceof Error)) {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const message = error.message.trim();
|
|
62
|
+
if (message.length === 0) {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const requestFailureMatch = message.match(/Request failed \((\d+)\)/i);
|
|
67
|
+
if (requestFailureMatch) {
|
|
68
|
+
return `${venueLabel} ${subject} request failed (${requestFailureMatch[1]}).`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
/typo in the url or port|access the url|unable to connect|could not resolve host|connection refused/i.test(
|
|
73
|
+
message,
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
return `${venueLabel} is unavailable right now.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
/socket connection|socket hang up|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch failed|network connection|connection closed/i.test(
|
|
81
|
+
message,
|
|
82
|
+
)
|
|
83
|
+
) {
|
|
84
|
+
return `${venueLabel} is unavailable right now.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return `${fallback} ${message}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
export function usePredictionMarketsDataState({
|
|
40
91
|
browseTab,
|
|
41
92
|
categoryId,
|
|
@@ -152,14 +203,84 @@ export function usePredictionMarketsDataState({
|
|
|
152
203
|
].filter((value): value is string => value != null),
|
|
153
204
|
[includeKalshi, includePolymarket, kalshiCatalogKey, polymarketCatalogKey],
|
|
154
205
|
);
|
|
206
|
+
const activeCatalogSources = useMemo(
|
|
207
|
+
() =>
|
|
208
|
+
[
|
|
209
|
+
includePolymarket
|
|
210
|
+
? {
|
|
211
|
+
venue: "polymarket" as const,
|
|
212
|
+
cacheKey: polymarketCatalogKey,
|
|
213
|
+
error: catalogErrors[polymarketCatalogKey] ?? null,
|
|
214
|
+
markets: polymarketCatalog,
|
|
215
|
+
}
|
|
216
|
+
: null,
|
|
217
|
+
includeKalshi
|
|
218
|
+
? {
|
|
219
|
+
venue: "kalshi" as const,
|
|
220
|
+
cacheKey: kalshiCatalogKey,
|
|
221
|
+
error: catalogErrors[kalshiCatalogKey] ?? null,
|
|
222
|
+
markets: kalshiCatalog,
|
|
223
|
+
}
|
|
224
|
+
: null,
|
|
225
|
+
].filter(
|
|
226
|
+
(
|
|
227
|
+
value,
|
|
228
|
+
): value is {
|
|
229
|
+
venue: PredictionVenue;
|
|
230
|
+
cacheKey: string;
|
|
231
|
+
error: string | null;
|
|
232
|
+
markets: PredictionMarketSummary[];
|
|
233
|
+
} => value != null,
|
|
234
|
+
),
|
|
235
|
+
[
|
|
236
|
+
catalogErrors,
|
|
237
|
+
includeKalshi,
|
|
238
|
+
includePolymarket,
|
|
239
|
+
kalshiCatalog,
|
|
240
|
+
kalshiCatalogKey,
|
|
241
|
+
polymarketCatalog,
|
|
242
|
+
polymarketCatalogKey,
|
|
243
|
+
],
|
|
244
|
+
);
|
|
155
245
|
const catalogLoadCount = activeCatalogKeys.reduce(
|
|
156
246
|
(count, cacheKey) => count + (catalogPending[cacheKey] ?? 0),
|
|
157
247
|
0,
|
|
158
248
|
);
|
|
159
|
-
const
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
|
|
249
|
+
const catalogStatus = useMemo(() => {
|
|
250
|
+
const failingSources = activeCatalogSources.filter((source) => !!source.error);
|
|
251
|
+
if (failingSources.length === 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const loadedSources = activeCatalogSources.filter(
|
|
256
|
+
(source) => !source.error && source.markets.length > 0,
|
|
257
|
+
);
|
|
258
|
+
if (failingSources.length < activeCatalogSources.length) {
|
|
259
|
+
const unavailableVenues = joinPredictionVenueLabels(
|
|
260
|
+
failingSources.map((source) => source.venue),
|
|
261
|
+
);
|
|
262
|
+
if (loadedSources.length > 0) {
|
|
263
|
+
return {
|
|
264
|
+
tone: "warning" as const,
|
|
265
|
+
message: `${unavailableVenues} unavailable right now; showing ${joinPredictionVenueLabels(
|
|
266
|
+
loadedSources.map((source) => source.venue),
|
|
267
|
+
)} markets.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
tone: "warning" as const,
|
|
272
|
+
message: `${unavailableVenues} unavailable right now.`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
tone: "danger" as const,
|
|
278
|
+
message: failingSources
|
|
279
|
+
.map((source) => source.error)
|
|
280
|
+
.filter((value): value is string => !!value)
|
|
281
|
+
.join(" "),
|
|
282
|
+
};
|
|
283
|
+
}, [activeCatalogSources]);
|
|
163
284
|
|
|
164
285
|
const allMarkets = useMemo(() => {
|
|
165
286
|
const merged: PredictionMarketSummary[] = [];
|
|
@@ -276,9 +397,7 @@ export function usePredictionMarketsDataState({
|
|
|
276
397
|
updatePredictionErrorState(
|
|
277
398
|
current,
|
|
278
399
|
cacheKey,
|
|
279
|
-
|
|
280
|
-
? error.message
|
|
281
|
-
: "Could not load Polymarket markets.",
|
|
400
|
+
formatPredictionLoadError("polymarket", "markets", error),
|
|
282
401
|
),
|
|
283
402
|
);
|
|
284
403
|
} finally {
|
|
@@ -310,9 +429,7 @@ export function usePredictionMarketsDataState({
|
|
|
310
429
|
updatePredictionErrorState(
|
|
311
430
|
current,
|
|
312
431
|
cacheKey,
|
|
313
|
-
|
|
314
|
-
? error.message
|
|
315
|
-
: "Could not load Kalshi markets.",
|
|
432
|
+
formatPredictionLoadError("kalshi", "markets", error),
|
|
316
433
|
),
|
|
317
434
|
);
|
|
318
435
|
} finally {
|
|
@@ -360,9 +477,7 @@ export function usePredictionMarketsDataState({
|
|
|
360
477
|
updatePredictionErrorState(
|
|
361
478
|
current,
|
|
362
479
|
cacheKey,
|
|
363
|
-
|
|
364
|
-
? error.message
|
|
365
|
-
: "Could not load market detail.",
|
|
480
|
+
formatPredictionLoadError(summary.venue, "market detail", error),
|
|
366
481
|
),
|
|
367
482
|
);
|
|
368
483
|
if (currentDetailCacheKeyRef.current === cacheKey) {
|
|
@@ -605,7 +720,7 @@ export function usePredictionMarketsDataState({
|
|
|
605
720
|
}, []);
|
|
606
721
|
|
|
607
722
|
return {
|
|
608
|
-
|
|
723
|
+
catalogStatus,
|
|
609
724
|
catalogLoadCount,
|
|
610
725
|
debouncedSearchQuery,
|
|
611
726
|
detail,
|
|
@@ -692,7 +692,7 @@ export function usePredictionMarketsController({
|
|
|
692
692
|
paneSettings,
|
|
693
693
|
browseTab,
|
|
694
694
|
categoryId,
|
|
695
|
-
|
|
695
|
+
catalogStatus: data.catalogStatus,
|
|
696
696
|
catalogLoadCount: data.catalogLoadCount,
|
|
697
697
|
detail: data.detail,
|
|
698
698
|
detailError: data.detailError,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core";
|
|
2
1
|
import { colors } from "../../../theme/colors";
|
|
3
|
-
import {
|
|
2
|
+
import { ExternalLinkText } from "../../../components/ui/external-link";
|
|
4
3
|
|
|
5
4
|
export function truncatePredictionText(
|
|
6
5
|
value: string,
|
|
@@ -19,10 +18,12 @@ export function SummaryLink({
|
|
|
19
18
|
maxLength: number;
|
|
20
19
|
}) {
|
|
21
20
|
return (
|
|
22
|
-
<box height={1}
|
|
23
|
-
<
|
|
24
|
-
{
|
|
25
|
-
|
|
21
|
+
<box height={1}>
|
|
22
|
+
<ExternalLinkText
|
|
23
|
+
url={url}
|
|
24
|
+
label={truncatePredictionText(url, maxLength)}
|
|
25
|
+
color={colors.textBright}
|
|
26
|
+
/>
|
|
26
27
|
</box>
|
|
27
28
|
);
|
|
28
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GloomPlugin, GloomPluginContext } from "../../types/plugin";
|
|
2
2
|
import { parsePredictionSearchShortcut } from "./navigation";
|
|
3
3
|
import { PredictionMarketsPane } from "./pane";
|
|
4
|
-
import { attachPredictionMarketsPersistence } from "./services/fetch";
|
|
4
|
+
import { attachPredictionMarketsPersistence, resetPredictionMarketsPersistence } from "./services/fetch";
|
|
5
5
|
import { predictionMarketsCliCommand } from "./cli";
|
|
6
6
|
import {
|
|
7
7
|
buildPredictionMarketsPaneSettingsDef,
|
|
@@ -101,4 +101,8 @@ export const predictionMarketsPlugin: GloomPlugin = {
|
|
|
101
101
|
},
|
|
102
102
|
});
|
|
103
103
|
},
|
|
104
|
+
|
|
105
|
+
dispose() {
|
|
106
|
+
resetPredictionMarketsPersistence();
|
|
107
|
+
},
|
|
104
108
|
};
|
|
@@ -83,6 +83,57 @@ describe("prediction markets pane interactions", () => {
|
|
|
83
83
|
expect(frame).toContain("Will the Fed cut rates?");
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test("shows a venue warning instead of a raw fetch error when one catalog fails", async () => {
|
|
87
|
+
attachPredictionMarketsPersistence(new MemoryPersistence());
|
|
88
|
+
|
|
89
|
+
globalThis.fetch = (async (input: Request | string | URL) => {
|
|
90
|
+
const url = String(input);
|
|
91
|
+
if (url.includes("gamma-api.polymarket.com")) {
|
|
92
|
+
throw new Error("Unable to connect. Was there a typo in the url or port?");
|
|
93
|
+
}
|
|
94
|
+
if (url.includes("/trade-api/v2/events?")) {
|
|
95
|
+
return new Response(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
events: [
|
|
98
|
+
{
|
|
99
|
+
title: "Fed series",
|
|
100
|
+
category: "Economics",
|
|
101
|
+
event_ticker: "FED-1",
|
|
102
|
+
series_ticker: "FED",
|
|
103
|
+
markets: [
|
|
104
|
+
{
|
|
105
|
+
ticker: "KAL-1",
|
|
106
|
+
title: "Will the Fed cut rates?",
|
|
107
|
+
yes_sub_title: "Yes",
|
|
108
|
+
event_ticker: "FED-1",
|
|
109
|
+
status: "open",
|
|
110
|
+
market_type: "binary",
|
|
111
|
+
last_price_dollars: "0.48",
|
|
112
|
+
volume_24h_fp: "15000",
|
|
113
|
+
volume_fp: "90000",
|
|
114
|
+
open_interest_fp: "45000",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
{ status: 200 },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
124
|
+
}) as unknown as typeof fetch;
|
|
125
|
+
|
|
126
|
+
testSetup = await testRender(<Harness />, { width: 120, height: 34 });
|
|
127
|
+
await flushFrames(testSetup);
|
|
128
|
+
|
|
129
|
+
const frame = testSetup.captureCharFrame();
|
|
130
|
+
expect(frame).toContain("Will the Fed cut rates?");
|
|
131
|
+
expect(frame).toContain(
|
|
132
|
+
"Polymarket unavailable right now; showing Kalshi markets.",
|
|
133
|
+
);
|
|
134
|
+
expect(frame).not.toContain("Was there a typo in the url or port?");
|
|
135
|
+
});
|
|
136
|
+
|
|
86
137
|
test("selects a market on single click and opens detail on double click", async () => {
|
|
87
138
|
installPredictionMarketMocks();
|
|
88
139
|
|
|
@@ -127,6 +178,33 @@ describe("prediction markets pane interactions", () => {
|
|
|
127
178
|
expect(frame).toContain("Kalshi primary rule");
|
|
128
179
|
});
|
|
129
180
|
|
|
181
|
+
test("focuses the pane when a market row is clicked", async () => {
|
|
182
|
+
installPredictionMarketMocks();
|
|
183
|
+
|
|
184
|
+
testSetup = await testRender(
|
|
185
|
+
<Harness initialFocusedPaneId="portfolio-list:main" />,
|
|
186
|
+
{ width: 120, height: 34 },
|
|
187
|
+
);
|
|
188
|
+
await flushFrames(testSetup);
|
|
189
|
+
|
|
190
|
+
expect(harnessStateRef.current?.focusedPaneId).toBe("portfolio-list:main");
|
|
191
|
+
|
|
192
|
+
const frame = testSetup.captureCharFrame();
|
|
193
|
+
const lines = frame.split("\n");
|
|
194
|
+
const kalshiRow = lines.findIndex((line) =>
|
|
195
|
+
line.includes("Will the Fed cut rates?"),
|
|
196
|
+
);
|
|
197
|
+
const kalshiCol = lines[kalshiRow]?.indexOf("Will the Fed cut rates?") ?? -1;
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
|
|
201
|
+
await testSetup!.renderOnce();
|
|
202
|
+
});
|
|
203
|
+
await flushFrames(testSetup);
|
|
204
|
+
|
|
205
|
+
expect(harnessStateRef.current?.focusedPaneId).toBe(TEST_PANE_ID);
|
|
206
|
+
});
|
|
207
|
+
|
|
130
208
|
test("loads selected detail once instead of refetching in a render loop", async () => {
|
|
131
209
|
const { fetchUrls } = installPredictionMarketMocks();
|
|
132
210
|
|
|
@@ -11,6 +11,10 @@ import type { PredictionBrowseTab, PredictionCategoryId } from "./types";
|
|
|
11
11
|
|
|
12
12
|
export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
13
13
|
const controller = usePredictionMarketsController({ focused });
|
|
14
|
+
const catalogStatusColor =
|
|
15
|
+
controller.catalogStatus?.tone === "danger"
|
|
16
|
+
? colors.negative
|
|
17
|
+
: colors.borderFocused;
|
|
14
18
|
|
|
15
19
|
const browseContent = (
|
|
16
20
|
<box
|
|
@@ -113,9 +117,9 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
113
117
|
</scrollbox>
|
|
114
118
|
) : null}
|
|
115
119
|
|
|
116
|
-
{controller.
|
|
117
|
-
<box height={1} paddingX={1}>
|
|
118
|
-
<text fg={
|
|
120
|
+
{controller.catalogStatus ? (
|
|
121
|
+
<box height={1} paddingX={1} width={width} backgroundColor={colors.panel}>
|
|
122
|
+
<text fg={catalogStatusColor}>{controller.catalogStatus.message}</text>
|
|
119
123
|
</box>
|
|
120
124
|
) : null}
|
|
121
125
|
|
|
@@ -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 },
|
|
@@ -19,18 +31,16 @@ export function attachPredictionMarketsPersistence(
|
|
|
19
31
|
predictionMarketsPersistence = persistence;
|
|
20
32
|
}
|
|
21
33
|
|
|
34
|
+
export function resetPredictionMarketsPersistence(): void {
|
|
35
|
+
predictionMarketsPersistence = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export function getPredictionMarketsPersistence(): PluginPersistence | null {
|
|
23
39
|
return predictionMarketsPersistence;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
export async function fetchJson<T>(url: string): Promise<T> {
|
|
27
|
-
const response = await fetch(url
|
|
28
|
-
signal: AbortSignal.timeout(10_000),
|
|
29
|
-
headers: {
|
|
30
|
-
Accept: "application/json",
|
|
31
|
-
"User-Agent": "gloomberb-prediction-markets",
|
|
32
|
-
},
|
|
33
|
-
});
|
|
43
|
+
const response = await PREDICTION_FETCH.fetch(url);
|
|
34
44
|
if (!response.ok) {
|
|
35
45
|
throw new Error(`Request failed (${response.status}) for ${url}`);
|
|
36
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
|
}
|
|
@@ -99,11 +99,22 @@ export class MemoryPersistence implements PluginPersistence {
|
|
|
99
99
|
|
|
100
100
|
attachPredictionMarketsPersistence(new MemoryPersistence());
|
|
101
101
|
|
|
102
|
-
export function createConfig(
|
|
102
|
+
export function createConfig(options?: {
|
|
103
|
+
initialFocusedPaneId?: string;
|
|
104
|
+
}): AppConfig {
|
|
103
105
|
const config = createDefaultConfig("/tmp/gloomberb-prediction-markets");
|
|
106
|
+
const initialFocusedPaneId = options?.initialFocusedPaneId ?? TEST_PANE_ID;
|
|
107
|
+
const extraInstances = initialFocusedPaneId !== TEST_PANE_ID
|
|
108
|
+
? [{
|
|
109
|
+
instanceId: initialFocusedPaneId,
|
|
110
|
+
paneId: initialFocusedPaneId.split(":")[0] ?? "portfolio-list",
|
|
111
|
+
binding: { kind: "none" as const },
|
|
112
|
+
}]
|
|
113
|
+
: [];
|
|
104
114
|
const layout = {
|
|
105
115
|
dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
|
|
106
116
|
instances: [
|
|
117
|
+
...extraInstances,
|
|
107
118
|
{
|
|
108
119
|
instanceId: TEST_PANE_ID,
|
|
109
120
|
paneId: "prediction-markets",
|
|
@@ -126,6 +137,8 @@ export function createRuntime(): PluginRuntimeAccess {
|
|
|
126
137
|
const listeners = new Map<string, Set<() => void>>();
|
|
127
138
|
|
|
128
139
|
return {
|
|
140
|
+
pinTicker() {},
|
|
141
|
+
navigateTicker() {},
|
|
129
142
|
subscribeResumeState(pluginId, key, listener) {
|
|
130
143
|
const listenerKey = `${pluginId}:${key}`;
|
|
131
144
|
if (!listeners.has(listenerKey)) listeners.set(listenerKey, new Set());
|
|
@@ -424,13 +437,17 @@ export function installPredictionMarketMocks() {
|
|
|
424
437
|
return { fetchUrls };
|
|
425
438
|
}
|
|
426
439
|
|
|
427
|
-
export function Harness(
|
|
440
|
+
export function Harness({
|
|
441
|
+
initialFocusedPaneId = TEST_PANE_ID,
|
|
442
|
+
}: {
|
|
443
|
+
initialFocusedPaneId?: string;
|
|
444
|
+
} = {}) {
|
|
428
445
|
const runtime = useMemo(() => createRuntime(), []);
|
|
429
446
|
const [state, dispatch] = useReducer(
|
|
430
447
|
appReducer,
|
|
431
448
|
(() => {
|
|
432
|
-
const initial = createInitialState(createConfig());
|
|
433
|
-
initial.focusedPaneId =
|
|
449
|
+
const initial = createInitialState(createConfig({ initialFocusedPaneId }));
|
|
450
|
+
initial.focusedPaneId = initialFocusedPaneId;
|
|
434
451
|
return initial;
|
|
435
452
|
})(),
|
|
436
453
|
);
|
|
@@ -442,7 +459,7 @@ export function Harness() {
|
|
|
442
459
|
<PredictionMarketsPane
|
|
443
460
|
paneId={TEST_PANE_ID}
|
|
444
461
|
paneType="prediction-markets"
|
|
445
|
-
focused
|
|
462
|
+
focused={state.focusedPaneId === TEST_PANE_ID}
|
|
446
463
|
width={120}
|
|
447
464
|
height={34}
|
|
448
465
|
/>
|