gloomberb 0.3.7 → 0.4.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.
Files changed (133) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +359 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +1 -1
  37. package/src/components/layout/pane-header.tsx +26 -2
  38. package/src/components/layout/shell.tsx +62 -50
  39. package/src/components/layout/status-bar.test.tsx +66 -0
  40. package/src/components/layout/status-bar.tsx +26 -5
  41. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  42. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  43. package/src/components/ticker-list-table.test.tsx +83 -0
  44. package/src/components/ticker-list-table.tsx +174 -97
  45. package/src/components/ui/data-table.tsx +27 -2
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/ui/page-stack-view.tsx +60 -0
  48. package/src/components/use-double-click-activation.ts +31 -0
  49. package/src/index.tsx +10 -10
  50. package/src/market-data/coordinator.test.ts +92 -4
  51. package/src/market-data/coordinator.ts +32 -5
  52. package/src/market-data/hooks.test.tsx +120 -0
  53. package/src/market-data/hooks.tsx +53 -16
  54. package/src/market-data/request-types.ts +4 -2
  55. package/src/market-data/selectors.ts +24 -58
  56. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  57. package/src/plugins/builtin/chat-controller.ts +310 -35
  58. package/src/plugins/builtin/chat.test.tsx +118 -6
  59. package/src/plugins/builtin/chat.tsx +175 -116
  60. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  61. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  62. package/src/plugins/builtin/news.tsx +1 -11
  63. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  64. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  65. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  66. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  67. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  68. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  69. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  70. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  71. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  72. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  73. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  74. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  75. package/src/plugins/builtin/sec.tsx +1 -11
  76. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  77. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  78. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  79. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  80. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  81. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  82. package/src/plugins/catalog.ts +65 -0
  83. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  84. package/src/plugins/ibkr/gateway-service.test.ts +45 -1
  85. package/src/plugins/ibkr/gateway-service.ts +141 -7
  86. package/src/plugins/pane-manager.test.ts +1 -1
  87. package/src/plugins/plugin-runtime.tsx +27 -7
  88. package/src/plugins/prediction-markets/cli.ts +28 -0
  89. package/src/plugins/prediction-markets/controller-data.ts +11 -3
  90. package/src/plugins/prediction-markets/controller.ts +141 -121
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/plugins/prediction-markets/launch.ts +51 -16
  93. package/src/plugins/prediction-markets/metrics.ts +9 -5
  94. package/src/plugins/prediction-markets/pane.test.tsx +119 -57
  95. package/src/plugins/prediction-markets/pane.tsx +74 -153
  96. package/src/plugins/prediction-markets/table.tsx +6 -3
  97. package/src/plugins/prediction-markets/types.ts +0 -1
  98. package/src/sources/gloomberb-cloud.test.ts +29 -0
  99. package/src/sources/gloomberb-cloud.ts +89 -1
  100. package/src/sources/provider-router.test.ts +265 -3
  101. package/src/sources/provider-router.ts +239 -11
  102. package/src/sources/yahoo-finance.test.ts +26 -0
  103. package/src/sources/yahoo-finance.ts +54 -3
  104. package/src/state/app-context-selector.test.tsx +99 -0
  105. package/src/state/app-context.test.ts +18 -0
  106. package/src/state/app-context.tsx +170 -65
  107. package/src/state/selectors.ts +25 -0
  108. package/src/theme/colors.ts +8 -8
  109. package/src/theme/themes.test.ts +2 -0
  110. package/src/theme/themes.ts +22 -10
  111. package/src/types/broker.ts +21 -0
  112. package/src/types/config.ts +17 -1
  113. package/src/types/data-provider.ts +18 -0
  114. package/src/types/financials.ts +31 -1
  115. package/src/types/plugin.ts +70 -0
  116. package/src/utils/api-client.test.ts +57 -0
  117. package/src/utils/api-client.ts +35 -12
  118. package/src/utils/back-navigation.ts +27 -0
  119. package/src/utils/format.test.ts +14 -0
  120. package/src/utils/format.ts +3 -3
  121. package/src/utils/market-status.ts +46 -1
  122. package/src/utils/quote-freshness.ts +127 -0
  123. package/src/utils/quote-resolution.test.ts +277 -0
  124. package/src/utils/quote-resolution.ts +641 -0
  125. package/src/utils/slugify.ts +3 -0
  126. package/src/utils/timestamp.ts +25 -0
  127. package/src/version.ts +1 -1
  128. package/src/cli/commands/portfolio.ts +0 -210
  129. package/src/cli/commands/watchlist.ts +0 -210
  130. package/src/plugins/builtin/data-providers.ts +0 -16
  131. package/src/sources/exchange-rates.ts +0 -18
  132. package/src/utils/ascii-chart.ts +0 -172
  133. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -25,6 +25,11 @@ import { tmpdir } from "os";
25
25
  import { join } from "path";
26
26
  import { firstValueFrom, filter, take, timeout } from "rxjs";
27
27
  import type { TimeRange } from "../../components/chart/chart-types";
28
+ import {
29
+ normalizeChartResolutionSupport,
30
+ type ChartResolutionSupport,
31
+ type ManualChartResolution,
32
+ } from "../../components/chart/chart-resolution";
28
33
  import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
29
34
  import type { QuoteSubscriptionTarget } from "../../types/data-provider";
30
35
  import type { Fundamentals, FinancialStatement, Quote, PricePoint, TickerFinancials } from "../../types/financials";
@@ -290,6 +295,7 @@ const IBKR_DATA_TIMEOUT = 8_000;
290
295
  const IBKR_QUOTE_STREAM_TICKS = "165,221,233";
291
296
 
292
297
  const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting }> = {
298
+ "1D": { duration: "1 D", size: BarSizeSetting.MINUTES_FIVE },
293
299
  "1W": { duration: "7 D", size: BarSizeSetting.HOURS_ONE },
294
300
  "1M": { duration: "1 M", size: BarSizeSetting.HOURS_ONE },
295
301
  "3M": { duration: "3 M", size: BarSizeSetting.DAYS_ONE },
@@ -299,6 +305,17 @@ const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting
299
305
  "ALL": { duration: "10 Y", size: BarSizeSetting.MONTHS_ONE },
300
306
  };
301
307
 
308
+ const IBKR_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
309
+ { resolution: "1m", maxRange: "1W" },
310
+ { resolution: "5m", maxRange: "1M" },
311
+ { resolution: "15m", maxRange: "3M" },
312
+ { resolution: "30m", maxRange: "6M" },
313
+ { resolution: "1h", maxRange: "1Y" },
314
+ { resolution: "1d", maxRange: "ALL" },
315
+ { resolution: "1wk", maxRange: "ALL" },
316
+ { resolution: "1mo", maxRange: "ALL" },
317
+ ]);
318
+
302
319
  /** Map generic bar size labels to IBKR BarSizeSetting values. */
303
320
  const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
304
321
  "1m": BarSizeSetting.MINUTES_ONE,
@@ -307,7 +324,8 @@ const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
307
324
  "30m": BarSizeSetting.MINUTES_THIRTY,
308
325
  "1h": BarSizeSetting.HOURS_ONE,
309
326
  "1d": BarSizeSetting.DAYS_ONE,
310
- "1w": BarSizeSetting.WEEKS_ONE,
327
+ "1wk": BarSizeSetting.WEEKS_ONE,
328
+ "1mo": BarSizeSetting.MONTHS_ONE,
311
329
  };
312
330
 
313
331
  export function parseIbkrHistoricalBarTime(value: string | number): Date {
@@ -383,6 +401,65 @@ function hasDelayedMarketData(
383
401
  ].some((tickType) => marketData.has(tickType));
384
402
  }
385
403
 
404
+ function firstNonSmartExchange(value?: string): string | undefined {
405
+ return value
406
+ ?.split(",")
407
+ .map((entry) => entry.trim())
408
+ .find((entry) => entry.length > 0 && entry !== "SMART");
409
+ }
410
+
411
+ function resolveIbkrListingExchange(
412
+ current: Quote | undefined,
413
+ contract: Contract,
414
+ details: ContractDetails,
415
+ ): string | undefined {
416
+ return current?.listingExchangeName
417
+ ?? contract.primaryExch
418
+ ?? firstNonSmartExchange(details.validExchanges)
419
+ ?? (current?.exchangeName !== "SMART" ? current?.exchangeName : undefined)
420
+ ?? (contract.exchange !== "SMART" ? contract.exchange : undefined);
421
+ }
422
+
423
+ function resolveIbkrRoutingExchange(current: Quote | undefined, contract: Contract): string | undefined {
424
+ return current?.routingExchangeName
425
+ ?? contract.exchange
426
+ ?? "SMART";
427
+ }
428
+
429
+ function ibkrVenueFields(
430
+ current: Quote | undefined,
431
+ contract: Contract,
432
+ details: ContractDetails,
433
+ ): Pick<
434
+ Quote,
435
+ | "exchangeName"
436
+ | "fullExchangeName"
437
+ | "listingExchangeName"
438
+ | "listingExchangeFullName"
439
+ | "routingExchangeName"
440
+ | "routingExchangeFullName"
441
+ | "marketState"
442
+ | "sessionConfidence"
443
+ > {
444
+ const listingExchangeName = resolveIbkrListingExchange(current, contract, details);
445
+ const listingExchangeFullName = current?.listingExchangeFullName
446
+ ?? current?.fullExchangeName
447
+ ?? listingExchangeName;
448
+ const routingExchangeName = resolveIbkrRoutingExchange(current, contract);
449
+ const routingExchangeFullName = current?.routingExchangeFullName ?? routingExchangeName;
450
+
451
+ return {
452
+ exchangeName: listingExchangeName,
453
+ fullExchangeName: listingExchangeFullName,
454
+ listingExchangeName,
455
+ listingExchangeFullName,
456
+ routingExchangeName,
457
+ routingExchangeFullName,
458
+ marketState: current?.marketState,
459
+ sessionConfidence: current?.sessionConfidence ?? "unknown",
460
+ };
461
+ }
462
+
386
463
  interface TickByTickBidAskUpdate {
387
464
  time: number;
388
465
  bidPrice: number;
@@ -417,9 +494,7 @@ export function applyTickByTickAllLastToQuote(
417
494
  volume: current?.volume,
418
495
  name: current?.name ?? details.longName ?? details.marketName ?? contract.symbol,
419
496
  lastUpdated: tick.time ? tick.time * 1000 : Date.now(),
420
- exchangeName: current?.exchangeName ?? details.validExchanges?.split(",")[0],
421
- fullExchangeName: current?.fullExchangeName ?? details.validExchanges?.split(",")[0],
422
- marketState: current?.marketState ?? "REGULAR",
497
+ ...ibkrVenueFields(current, contract, details),
423
498
  preMarketPrice: current?.preMarketPrice,
424
499
  preMarketChange: current?.preMarketChange,
425
500
  preMarketChangePercent: current?.preMarketChangePercent,
@@ -1052,6 +1127,67 @@ export class IbkrGatewayService {
1052
1127
  });
1053
1128
  }
1054
1129
 
1130
+ getChartResolutionSupport(
1131
+ _ticker: string,
1132
+ _config: IbkrGatewayConfig,
1133
+ _exchange?: string,
1134
+ _instrument?: BrokerContractRef | null,
1135
+ ): ChartResolutionSupport[] {
1136
+ return IBKR_RESOLUTION_SUPPORT;
1137
+ }
1138
+
1139
+ getChartResolutionCapabilities(
1140
+ _ticker: string,
1141
+ _config: IbkrGatewayConfig,
1142
+ _exchange?: string,
1143
+ _instrument?: BrokerContractRef | null,
1144
+ ): ManualChartResolution[] {
1145
+ return IBKR_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
1146
+ }
1147
+
1148
+ async getPriceHistoryForResolution(
1149
+ ticker: string,
1150
+ config: IbkrGatewayConfig,
1151
+ exchange: string,
1152
+ bufferRange: TimeRange,
1153
+ resolution: ManualChartResolution,
1154
+ instrument?: BrokerContractRef | null,
1155
+ ): Promise<PricePoint[]> {
1156
+ const ibkrBarSize = GENERIC_BAR_SIZE_MAP[resolution];
1157
+ if (!ibkrBarSize) return [];
1158
+
1159
+ return this.dedup(`history:${ticker}:${exchange}:${bufferRange}:${resolution}`, async () => {
1160
+ await this.connect(config);
1161
+ const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
1162
+ const params = HISTORY_PARAMS[bufferRange];
1163
+ const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
1164
+ .catch(() => undefined);
1165
+ const bars = await this.withMarketDataFallback(
1166
+ config,
1167
+ () => withTimeout(this.api!.getHistoricalData(
1168
+ contract,
1169
+ "",
1170
+ params.duration,
1171
+ ibkrBarSize,
1172
+ WhatToShow.TRADES,
1173
+ 1,
1174
+ 1,
1175
+ ), IBKR_DATA_TIMEOUT, "getHistoricalData"),
1176
+ );
1177
+ const details = await detailsPromise;
1178
+ const priceDivisor = getIbkrPriceDivisor(contract, details);
1179
+
1180
+ return bars.map((bar) => ({
1181
+ date: parseIbkrHistoricalBarTime(bar.time),
1182
+ open: normalizeIbkrPriceValue(bar.open, priceDivisor),
1183
+ high: normalizeIbkrPriceValue(bar.high, priceDivisor),
1184
+ low: normalizeIbkrPriceValue(bar.low, priceDivisor),
1185
+ close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
1186
+ volume: bar.volume,
1187
+ }));
1188
+ });
1189
+ }
1190
+
1055
1191
  async getDetailedPriceHistory(
1056
1192
  ticker: string,
1057
1193
  config: IbkrGatewayConfig,
@@ -2150,9 +2286,7 @@ export class IbkrGatewayService {
2150
2286
  volume: marketData.get(IBApiTickType.VOLUME)?.value ?? marketData.get(IBApiTickType.DELAYED_VOLUME)?.value,
2151
2287
  name: details.longName || details.marketName || contract.symbol,
2152
2288
  lastUpdated: ingressTm,
2153
- exchangeName: details.validExchanges?.split(",")[0],
2154
- fullExchangeName: details.validExchanges?.split(",")[0],
2155
- marketState: "REGULAR",
2289
+ ...ibkrVenueFields(undefined, contract, details),
2156
2290
  bid: normalizeIbkrPriceValue(
2157
2291
  marketData.get(IBApiTickType.BID)?.value ?? marketData.get(IBApiTickType.DELAYED_BID)?.value,
2158
2292
  priceDivisor,
@@ -71,7 +71,7 @@ describe("pane-manager split-tree drops", () => {
71
71
  const next = gridlockAllPanes(layout);
72
72
 
73
73
  expect(next.floating).toHaveLength(0);
74
- expect(getDockedPaneIds(next)).toHaveLength(4);
74
+ expect(getDockedPaneIds(next)).toHaveLength(5);
75
75
  });
76
76
 
77
77
  test("gridlock infers a matching tiled layout from arranged windows", () => {
@@ -2,6 +2,7 @@ import {
2
2
  createContext,
3
3
  useCallback,
4
4
  useContext,
5
+ useRef,
5
6
  useSyncExternalStore,
6
7
  type ReactNode,
7
8
  type SetStateAction,
@@ -101,12 +102,21 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
101
102
  const { pluginId } = usePluginRenderContext();
102
103
  const scopedPaneId = paneId ?? usePaneInstanceId();
103
104
  const { state, dispatch } = useAppState();
105
+ const stateRef = useRef(state);
106
+ const fallbackRef = useRef(fallback);
107
+ stateRef.current = state;
108
+ fallbackRef.current = fallback;
104
109
  const paneState = state.paneState[scopedPaneId];
105
110
  const value = getPluginPaneStateValue(paneState, pluginId, key, fallback);
106
111
 
107
112
  const setValue = useCallback((nextValue: SetStateAction<T>) => {
108
- const currentPaneState = state.paneState[scopedPaneId];
109
- const currentValue = getPluginPaneStateValue(currentPaneState, pluginId, key, fallback);
113
+ const currentPaneState = stateRef.current.paneState[scopedPaneId];
114
+ const currentValue = getPluginPaneStateValue(
115
+ currentPaneState,
116
+ pluginId,
117
+ key,
118
+ fallbackRef.current,
119
+ );
110
120
  const resolved = typeof nextValue === "function"
111
121
  ? (nextValue as (previousValue: T) => T)(currentValue)
112
122
  : nextValue;
@@ -122,7 +132,7 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
122
132
  key,
123
133
  value: resolved,
124
134
  });
125
- }, [dispatch, fallback, key, pluginId, scopedPaneId, state.paneState]);
135
+ }, [dispatch, key, pluginId, scopedPaneId]);
126
136
 
127
137
  return [value, setValue];
128
138
  }
@@ -130,6 +140,8 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
130
140
  export function usePluginState<T>(key: string, fallback: T, options?: { schemaVersion?: number }): [T, (value: SetStateAction<T>) => void] {
131
141
  const { pluginId, runtime } = usePluginRenderContext();
132
142
  const schemaVersion = options?.schemaVersion;
143
+ const fallbackRef = useRef(fallback);
144
+ fallbackRef.current = fallback;
133
145
 
134
146
  const subscribe = useCallback((listener: () => void) => (
135
147
  runtime.subscribeResumeState(pluginId, key, listener)
@@ -142,7 +154,9 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
142
154
  const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
143
155
 
144
156
  const setValue = useCallback((nextValue: SetStateAction<T>) => {
145
- const currentValue = runtime.getResumeState<T>(pluginId, key, schemaVersion) ?? fallback;
157
+ const currentValue =
158
+ runtime.getResumeState<T>(pluginId, key, schemaVersion) ??
159
+ fallbackRef.current;
146
160
  const resolved = typeof nextValue === "function"
147
161
  ? (nextValue as (previousValue: T) => T)(currentValue)
148
162
  : nextValue;
@@ -150,7 +164,7 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
150
164
  return;
151
165
  }
152
166
  runtime.setResumeState(pluginId, key, resolved, schemaVersion);
153
- }, [fallback, key, pluginId, runtime, schemaVersion]);
167
+ }, [key, pluginId, runtime, schemaVersion]);
154
168
 
155
169
  return [value, setValue];
156
170
  }
@@ -158,10 +172,16 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
158
172
  export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: SetStateAction<T>) => void] {
159
173
  const { pluginId, runtime } = usePluginRenderContext();
160
174
  const { state } = useAppState();
175
+ const stateRef = useRef(state);
176
+ const fallbackRef = useRef(fallback);
177
+ stateRef.current = state;
178
+ fallbackRef.current = fallback;
161
179
  const value = (state.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? fallback;
162
180
 
163
181
  const setValue = useCallback((nextValue: SetStateAction<T>) => {
164
- const currentValue = (state.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? fallback;
182
+ const currentValue =
183
+ (stateRef.current.config.pluginConfig[pluginId]?.[key] as T | undefined) ??
184
+ fallbackRef.current;
165
185
  const resolved = typeof nextValue === "function"
166
186
  ? (nextValue as (previousValue: T) => T)(currentValue)
167
187
  : nextValue;
@@ -169,7 +189,7 @@ export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: S
169
189
  return;
170
190
  }
171
191
  void runtime.setConfigState(pluginId, key, resolved);
172
- }, [fallback, key, pluginId, runtime, state.config.pluginConfig]);
192
+ }, [key, pluginId, runtime]);
173
193
 
174
194
  return [value, setValue];
175
195
  }
@@ -0,0 +1,28 @@
1
+ import type { CliCommandDef } from "../../types/plugin";
2
+ import { createPredictionLaunchRequest, parsePredictionCommandArgs } from "./launch";
3
+
4
+ export const predictionMarketsCliCommand: CliCommandDef = {
5
+ name: "predictions",
6
+ aliases: ["prediction-markets", "pm"],
7
+ description: "Launch the UI with Prediction Markets focused",
8
+ help: {
9
+ usage: ["predictions [...]"],
10
+ sections: [{
11
+ title: "Prediction Launch",
12
+ columns: [
13
+ { header: "Input" },
14
+ { header: "Example" },
15
+ ],
16
+ rows: [
17
+ ["gloomberb predictions [venue] [category] [browse-tab] [search...]", "gloomberb predictions world"],
18
+ ["venue", "all | polymarket | kalshi"],
19
+ ["category", "all | politics | world | macro | crypto | science | sports | entertainment | climate | social"],
20
+ ["browse-tab", "top | ending | new | watchlist"],
21
+ ],
22
+ }],
23
+ },
24
+ execute: async (args) => ({
25
+ kind: "launch-ui",
26
+ request: createPredictionLaunchRequest(parsePredictionCommandArgs(args)),
27
+ }),
28
+ };
@@ -39,6 +39,7 @@ const KEYBOARD_DETAIL_LOAD_DELAY_MS = 140;
39
39
  export function usePredictionMarketsDataState({
40
40
  browseTab,
41
41
  categoryId,
42
+ detailOpen,
42
43
  effectiveVenueScope,
43
44
  focused,
44
45
  historyRange,
@@ -52,6 +53,7 @@ export function usePredictionMarketsDataState({
52
53
  }: {
53
54
  browseTab: PredictionBrowseTab;
54
55
  categoryId: PredictionCategoryId;
56
+ detailOpen: boolean;
55
57
  effectiveVenueScope: PredictionVenueScope;
56
58
  focused: boolean;
57
59
  historyRange: "1D" | "1W" | "1M" | "ALL";
@@ -201,7 +203,11 @@ export function usePredictionMarketsDataState({
201
203
  [selectedRowKey, visibleRows],
202
204
  );
203
205
  const selectedSummary = useMemo(
204
- () =>
206
+ () => {
207
+ if (!detailOpen || !selectedRow) {
208
+ return null;
209
+ }
210
+ return (
205
211
  selectedRow?.markets.find(
206
212
  (market) => market.key === selectedDetailMarketKey,
207
213
  ) ??
@@ -209,8 +215,10 @@ export function usePredictionMarketsDataState({
209
215
  (market) => market.key === selectedRow.focusMarketKey,
210
216
  ) ??
211
217
  selectedRow?.representative ??
212
- null,
213
- [selectedDetailMarketKey, selectedRow],
218
+ null
219
+ );
220
+ },
221
+ [detailOpen, selectedDetailMarketKey, selectedRow],
214
222
  );
215
223
  const selectedSummaryKey = selectedSummary?.key ?? null;
216
224
  const selectedSummaryVenue = selectedSummary?.venue ?? null;