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
@@ -21,7 +21,6 @@ import type {
21
21
  PredictionBrowseTab,
22
22
  PredictionCategoryId,
23
23
  PredictionDetailTab,
24
- PredictionFocusRegion,
25
24
  PredictionHistoryRange,
26
25
  PredictionListRow,
27
26
  PredictionOrderPreviewIntent,
@@ -29,7 +28,6 @@ import type {
29
28
  PredictionVenueScope,
30
29
  } from "./types";
31
30
 
32
- const EMPTY_SELECTIONS: Record<string, string | null> = {};
33
31
  const KEYBOARD_DETAIL_LOAD_DELAY_MS = 140;
34
32
 
35
33
  export function usePredictionMarketsController({
@@ -63,10 +61,6 @@ export function usePredictionMarketsController({
63
61
  "detailTab",
64
62
  "overview",
65
63
  );
66
- const [focusRegion, setFocusRegion] = usePluginPaneState<PredictionFocusRegion>(
67
- "focusRegion",
68
- "list",
69
- );
70
64
  const [searchQuery, setSearchQuery] = usePluginPaneState<string>(
71
65
  "searchQuery",
72
66
  "",
@@ -77,22 +71,19 @@ export function usePredictionMarketsController({
77
71
  );
78
72
  const [historyRange, setHistoryRange] =
79
73
  usePluginPaneState<PredictionHistoryRange>("historyRange", "1M");
80
- const [detailSplitRatio, setDetailSplitRatio] = usePluginPaneState<number>(
81
- "detailSplitRatio",
82
- 0.42,
83
- );
84
74
  const [selectedRowKey, setSelectedRowKey] = usePluginPaneState<
85
75
  string | null
86
76
  >("selectedRowKey", null);
87
77
  const [selectedDetailMarketKey, setSelectedDetailMarketKey] =
88
78
  usePluginPaneState<string | null>("selectedDetailMarketKey", null);
89
- const [, setSelectionByScope] = usePluginPaneState<
90
- Record<string, string | null>
91
- >("selectionByScope", EMPTY_SELECTIONS);
79
+ const defaultSortPreference = useMemo(
80
+ () => getDefaultPredictionSort(paneSettings.defaultBrowseTab),
81
+ [paneSettings.defaultBrowseTab],
82
+ );
92
83
  const [sortPreference, setSortPreference] =
93
84
  usePluginPaneState<PredictionSortPreference>(
94
85
  "sortPreference",
95
- getDefaultPredictionSort(paneSettings.defaultBrowseTab),
86
+ defaultSortPreference,
96
87
  );
97
88
  const [, setOrderPreviewIntent] =
98
89
  usePluginPaneState<PredictionOrderPreviewIntent | null>(
@@ -101,6 +92,7 @@ export function usePredictionMarketsController({
101
92
  );
102
93
 
103
94
  const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
95
+ const [detailOpen, setDetailOpen] = useState(false);
104
96
  const [searchFocused, setSearchFocused] = useState(false);
105
97
  const [initialParamsApplied, setInitialParamsApplied] = useState(false);
106
98
 
@@ -126,6 +118,7 @@ export function usePredictionMarketsController({
126
118
  const data = usePredictionMarketsDataState({
127
119
  browseTab,
128
120
  categoryId,
121
+ detailOpen,
129
122
  effectiveVenueScope,
130
123
  focused,
131
124
  historyRange,
@@ -197,14 +190,13 @@ export function usePredictionMarketsController({
197
190
  ]);
198
191
 
199
192
  useEffect(() => {
200
- const nextDefault = getDefaultPredictionSort(browseTab);
201
193
  setSortPreference((current) =>
202
- current.columnId === nextDefault.columnId &&
203
- current.direction === nextDefault.direction
194
+ current.columnId === defaultSortPreference.columnId &&
195
+ current.direction === defaultSortPreference.direction
204
196
  ? current
205
- : nextDefault,
197
+ : defaultSortPreference,
206
198
  );
207
- }, [browseTab, setSortPreference]);
199
+ }, [defaultSortPreference, setSortPreference]);
208
200
 
209
201
  useEffect(() => {
210
202
  const nextFilterResetKey = [
@@ -231,7 +223,7 @@ export function usePredictionMarketsController({
231
223
  if (headerScrollBox) {
232
224
  headerScrollBox.scrollLeft = 0;
233
225
  }
234
- setFocusRegion("list");
226
+ setDetailOpen(false);
235
227
  setSelectedRowKey((current) => (current == null ? current : null));
236
228
  setSelectedDetailMarketKey((current) => (current == null ? current : null));
237
229
  }, [
@@ -239,54 +231,73 @@ export function usePredictionMarketsController({
239
231
  categoryId,
240
232
  data.debouncedSearchQuery,
241
233
  effectiveVenueScope,
242
- setFocusRegion,
243
234
  setSelectedDetailMarketKey,
244
235
  setSelectedRowKey,
245
236
  ]);
246
237
 
247
238
  useEffect(() => {
248
239
  if (data.visibleRows.length === 0) {
249
- if (selectedRowKey !== null) setSelectedRowKey(null);
250
- if (selectedDetailMarketKey !== null) setSelectedDetailMarketKey(null);
251
- if (focusRegion !== "list") setFocusRegion("list");
252
- return;
240
+ if (selectedRowKey !== null) {
241
+ setSelectedRowKey(null);
242
+ }
243
+ if (selectedDetailMarketKey !== null) {
244
+ setSelectedDetailMarketKey(null);
245
+ }
246
+ if (detailOpen) {
247
+ setDetailOpen(false);
248
+ }
253
249
  }
250
+ }, [
251
+ data.visibleRows.length,
252
+ detailOpen,
253
+ selectedDetailMarketKey,
254
+ selectedRowKey,
255
+ setSelectedDetailMarketKey,
256
+ setSelectedRowKey,
257
+ ]);
254
258
 
255
- if (data.selectedRow) {
256
- setSelectionByScope((current) => {
257
- if (current[effectiveVenueScope] === data.selectedRow!.key) {
258
- return current;
259
- }
260
- return { ...current, [effectiveVenueScope]: data.selectedRow!.key };
261
- });
262
- if (
263
- !selectedDetailMarketKey ||
264
- !data.selectedRow.markets.some(
265
- (market) => market.key === selectedDetailMarketKey,
266
- )
267
- ) {
268
- setSelectedDetailMarketKey(data.selectedRow.focusMarketKey);
269
- }
259
+ useEffect(() => {
260
+ if (selectedRowKey == null || data.selectedRow) {
270
261
  return;
271
262
  }
272
263
 
273
- if (selectedRowKey !== null) {
274
- setSelectedRowKey(null);
264
+ if (detailOpen) {
265
+ setDetailOpen(false);
275
266
  }
276
267
  if (selectedDetailMarketKey !== null) {
277
268
  setSelectedDetailMarketKey(null);
278
269
  }
270
+ setSelectedRowKey(null);
279
271
  }, [
280
272
  data.selectedRow,
281
- data.visibleRows,
282
- effectiveVenueScope,
283
- focusRegion,
273
+ detailOpen,
284
274
  selectedDetailMarketKey,
285
275
  selectedRowKey,
286
- setFocusRegion,
276
+ setDetailOpen,
287
277
  setSelectedDetailMarketKey,
288
278
  setSelectedRowKey,
289
- setSelectionByScope,
279
+ ]);
280
+
281
+ useEffect(() => {
282
+ if (!detailOpen || !data.selectedRow) {
283
+ return;
284
+ }
285
+
286
+ if (
287
+ selectedDetailMarketKey
288
+ && data.selectedRow.markets.some(
289
+ (market) => market.key === selectedDetailMarketKey,
290
+ )
291
+ ) {
292
+ return;
293
+ }
294
+
295
+ setSelectedDetailMarketKey(data.selectedRow.focusMarketKey);
296
+ }, [
297
+ data.selectedRow,
298
+ detailOpen,
299
+ selectedDetailMarketKey,
300
+ setSelectedDetailMarketKey,
290
301
  ]);
291
302
 
292
303
  useEffect(() => {
@@ -312,9 +323,8 @@ export function usePredictionMarketsController({
312
323
  }, [searchFocused]);
313
324
 
314
325
  const focusSearch = useCallback(() => {
315
- setFocusRegion("list");
316
326
  setSearchFocused(true);
317
- }, [setFocusRegion]);
327
+ }, []);
318
328
 
319
329
  const blurSearch = useCallback(() => {
320
330
  setSearchFocused(false);
@@ -336,52 +346,51 @@ export function usePredictionMarketsController({
336
346
  [setWatchlist],
337
347
  );
338
348
 
339
- const selectRow = useCallback(
349
+ const setBrowseSelection = useCallback(
340
350
  (rowKey: string, options?: { debounceDetail?: boolean }) => {
341
351
  setSearchFocused(false);
342
- setFocusRegion("list");
343
- const row = data.visibleRows.find((candidate) => candidate.key === rowKey);
344
- const shouldClear = selectedRowKey === rowKey;
345
352
  data.actions.setNextDetailLoadDelay(
346
- options?.debounceDetail && !shouldClear
347
- ? KEYBOARD_DETAIL_LOAD_DELAY_MS
348
- : 0,
349
- );
350
- setSelectedRowKey(shouldClear ? null : rowKey);
351
- setSelectedDetailMarketKey(
352
- shouldClear ? null : row?.focusMarketKey ?? null,
353
+ options?.debounceDetail ? KEYBOARD_DETAIL_LOAD_DELAY_MS : 0,
353
354
  );
355
+ setSelectedRowKey(rowKey);
354
356
  },
355
- [
356
- data.actions,
357
- data.visibleRows,
358
- selectedRowKey,
359
- setFocusRegion,
360
- setSelectedDetailMarketKey,
361
- setSelectedRowKey,
362
- ],
357
+ [data.actions, setSelectedRowKey],
358
+ );
359
+
360
+ const openSelectedRow = useCallback(
361
+ (rowKey: string) => {
362
+ const row = data.visibleRows.find((candidate) => candidate.key === rowKey);
363
+ if (!row) return;
364
+ setSearchFocused(false);
365
+ data.actions.setNextDetailLoadDelay(0);
366
+ setSelectedRowKey(row.key);
367
+ setSelectedDetailMarketKey(row.focusMarketKey);
368
+ setDetailOpen(true);
369
+ },
370
+ [data.actions, data.visibleRows, setSelectedDetailMarketKey, setSelectedRowKey],
363
371
  );
364
372
 
365
373
  const selectMarket = useCallback(
366
374
  (marketKey: string) => {
367
375
  setSearchFocused(false);
368
- setFocusRegion("detail");
369
376
  data.actions.setNextDetailLoadDelay(0);
370
377
  const row = data.visibleRows.find((candidate) =>
371
378
  candidate.markets.some((market) => market.key === marketKey),
372
379
  );
373
380
  if (!row) {
381
+ setDetailOpen(false);
374
382
  setSelectedRowKey(null);
375
383
  setSelectedDetailMarketKey(null);
376
384
  return;
377
385
  }
378
386
  setSelectedRowKey(row.key);
379
387
  setSelectedDetailMarketKey(marketKey);
388
+ setDetailOpen(true);
380
389
  },
381
390
  [
382
391
  data.actions,
383
392
  data.visibleRows,
384
- setFocusRegion,
393
+ setDetailOpen,
385
394
  setSelectedDetailMarketKey,
386
395
  setSelectedRowKey,
387
396
  ],
@@ -389,26 +398,34 @@ export function usePredictionMarketsController({
389
398
 
390
399
  const clearSelection = useCallback(() => {
391
400
  data.actions.setNextDetailLoadDelay(0);
392
- setFocusRegion("list");
401
+ setDetailOpen(false);
393
402
  setSelectedRowKey(null);
394
403
  setSelectedDetailMarketKey(null);
395
404
  }, [
396
405
  data.actions,
397
- setFocusRegion,
406
+ setDetailOpen,
398
407
  setSelectedDetailMarketKey,
399
408
  setSelectedRowKey,
400
409
  ]);
401
410
 
402
- const focusList = useCallback(() => {
403
- setFocusRegion("list");
411
+ const closeDetail = useCallback(() => {
412
+ setDetailOpen(false);
404
413
  setSearchFocused(false);
405
- }, [setFocusRegion]);
414
+ }, []);
406
415
 
407
- const focusDetail = useCallback(() => {
408
- if (!data.selectedSummary) return;
409
- setFocusRegion("detail");
416
+ const openDetail = useCallback(() => {
417
+ if (!data.selectedRow) return;
418
+ if (
419
+ !selectedDetailMarketKey ||
420
+ !data.selectedRow.markets.some(
421
+ (market) => market.key === selectedDetailMarketKey,
422
+ )
423
+ ) {
424
+ setSelectedDetailMarketKey(data.selectedRow.focusMarketKey);
425
+ }
426
+ setDetailOpen(true);
410
427
  setSearchFocused(false);
411
- }, [data.selectedSummary, setFocusRegion]);
428
+ }, [data.selectedRow, selectedDetailMarketKey, setSelectedDetailMarketKey]);
412
429
 
413
430
  const setVenue = useCallback(
414
431
  (nextVenueScope: string) => {
@@ -490,6 +507,7 @@ export function usePredictionMarketsController({
490
507
  name?: string;
491
508
  sequence?: string;
492
509
  shift?: boolean;
510
+ preventDefault?: () => void;
493
511
  stopPropagation?: () => void;
494
512
  }) => {
495
513
  if (!focused) return;
@@ -500,40 +518,16 @@ export function usePredictionMarketsController({
500
518
  if (searchFocused) {
501
519
  if (command === "escape") {
502
520
  event.stopPropagation?.();
521
+ event.preventDefault?.();
503
522
  setSearchFocused(false);
504
523
  }
505
524
  return;
506
525
  }
507
526
 
508
- if (command === "search") {
509
- event.stopPropagation?.();
510
- focusSearch();
511
- return;
512
- }
513
-
514
- if (command === "escape") {
515
- event.stopPropagation?.();
516
- if (focusRegion === "detail") {
517
- focusList();
518
- return;
519
- }
520
- clearSelection();
521
- return;
522
- }
523
-
524
- if (isEnter && data.selectedSummary) {
525
- event.stopPropagation?.();
526
- if (focusRegion === "list") {
527
- focusDetail();
528
- } else {
529
- focusList();
530
- }
531
- return;
532
- }
533
-
534
- if (focusRegion === "detail") {
527
+ if (detailOpen) {
535
528
  if (command === "move-down") {
536
529
  event.stopPropagation?.();
530
+ event.preventDefault?.();
537
531
  if (detailTab === "overview" && data.sortedOutcomeMarkets.length > 0) {
538
532
  cycleDetailOutcome("next");
539
533
  } else {
@@ -544,6 +538,7 @@ export function usePredictionMarketsController({
544
538
 
545
539
  if (command === "move-up") {
546
540
  event.stopPropagation?.();
541
+ event.preventDefault?.();
547
542
  if (detailTab === "overview" && data.sortedOutcomeMarkets.length > 0) {
548
543
  cycleDetailOutcome("previous");
549
544
  } else {
@@ -554,39 +549,64 @@ export function usePredictionMarketsController({
554
549
 
555
550
  if (command === "previous-category") {
556
551
  event.stopPropagation?.();
552
+ event.preventDefault?.();
557
553
  setDetailTab(getAdjacentPredictionDetailTab(detailTab, "previous"));
558
554
  return;
559
555
  }
560
556
 
561
557
  if (command === "next-category") {
562
558
  event.stopPropagation?.();
559
+ event.preventDefault?.();
563
560
  setDetailTab(getAdjacentPredictionDetailTab(detailTab, "next"));
564
561
  return;
565
562
  }
563
+
564
+ return;
565
+ }
566
+
567
+ if (command === "search") {
568
+ event.stopPropagation?.();
569
+ event.preventDefault?.();
570
+ focusSearch();
571
+ return;
572
+ }
573
+
574
+ if (isEnter && data.selectedRow) {
575
+ event.stopPropagation?.();
576
+ event.preventDefault?.();
577
+ openDetail();
578
+ return;
566
579
  }
567
580
 
568
581
  if (command === "move-down") {
569
582
  event.stopPropagation?.();
583
+ event.preventDefault?.();
570
584
  if (data.visibleRows.length === 0) return;
571
585
  const nextIndex =
572
586
  data.selectedIndex >= 0
573
587
  ? Math.min(data.selectedIndex + 1, data.visibleRows.length - 1)
574
588
  : 0;
575
- selectRow(data.visibleRows[nextIndex]!.key, { debounceDetail: true });
589
+ setBrowseSelection(data.visibleRows[nextIndex]!.key, {
590
+ debounceDetail: true,
591
+ });
576
592
  return;
577
593
  }
578
594
 
579
595
  if (command === "move-up") {
580
596
  event.stopPropagation?.();
597
+ event.preventDefault?.();
581
598
  if (data.visibleRows.length === 0) return;
582
599
  const nextIndex =
583
600
  data.selectedIndex >= 0 ? Math.max(data.selectedIndex - 1, 0) : 0;
584
- selectRow(data.visibleRows[nextIndex]!.key, { debounceDetail: true });
601
+ setBrowseSelection(data.visibleRows[nextIndex]!.key, {
602
+ debounceDetail: true,
603
+ });
585
604
  return;
586
605
  }
587
606
 
588
607
  if (!paneSettings.hideTabs && command === "previous-venue-tab") {
589
608
  event.stopPropagation?.();
609
+ event.preventDefault?.();
590
610
  setVenue(
591
611
  getAdjacentPredictionVenueScope(effectiveVenueScope, "previous"),
592
612
  );
@@ -595,24 +615,28 @@ export function usePredictionMarketsController({
595
615
 
596
616
  if (!paneSettings.hideTabs && command === "next-venue-tab") {
597
617
  event.stopPropagation?.();
618
+ event.preventDefault?.();
598
619
  setVenue(getAdjacentPredictionVenueScope(effectiveVenueScope, "next"));
599
620
  return;
600
621
  }
601
622
 
602
623
  if (command === "previous-category") {
603
624
  event.stopPropagation?.();
625
+ event.preventDefault?.();
604
626
  selectCategory(getAdjacentPredictionCategoryId(categoryId, "previous"));
605
627
  return;
606
628
  }
607
629
 
608
630
  if (command === "next-category") {
609
631
  event.stopPropagation?.();
632
+ event.preventDefault?.();
610
633
  selectCategory(getAdjacentPredictionCategoryId(categoryId, "next"));
611
634
  return;
612
635
  }
613
636
 
614
- if (command === "toggle-watchlist" && data.selectedSummary) {
637
+ if (command === "toggle-watchlist" && data.selectedRow) {
615
638
  event.stopPropagation?.();
639
+ event.preventDefault?.();
616
640
  if (data.selectedRow) toggleWatchlist(data.selectedRow);
617
641
  return;
618
642
  }
@@ -636,26 +660,23 @@ export function usePredictionMarketsController({
636
660
  [
637
661
  browseTab,
638
662
  categoryId,
639
- clearSelection,
640
663
  cycleDetailOutcome,
664
+ detailOpen,
641
665
  data.selectedIndex,
642
666
  data.selectedRow,
643
- data.selectedSummary,
644
667
  data.sortedOutcomeMarkets.length,
645
668
  data.visibleRows,
646
669
  detailTab,
647
670
  effectiveVenueScope,
648
- focusDetail,
649
- focusList,
650
- focusRegion,
651
671
  focusSearch,
652
672
  focused,
673
+ openDetail,
653
674
  paneSettings.hideTabs,
654
675
  scrollDetailBy,
655
676
  searchFocused,
656
677
  selectBrowseTab,
657
678
  selectCategory,
658
- selectRow,
679
+ setBrowseSelection,
659
680
  setVenue,
660
681
  toggleWatchlist,
661
682
  ],
@@ -676,11 +697,10 @@ export function usePredictionMarketsController({
676
697
  detail: data.detail,
677
698
  detailError: data.detailError,
678
699
  detailLoadCount: data.detailLoadCount,
700
+ detailOpen,
679
701
  detailScrollRef,
680
702
  detailTab,
681
- detailSplitRatio,
682
703
  effectiveVenueScope,
683
- focusRegion,
684
704
  headerScrollRef,
685
705
  historyRange,
686
706
  hoveredIdx,
@@ -699,18 +719,18 @@ export function usePredictionMarketsController({
699
719
  watchlistSet,
700
720
  actions: {
701
721
  blurSearch,
722
+ closeDetail,
702
723
  clearSelection,
703
- focusDetail,
704
- focusList,
705
724
  focusSearch,
706
725
  handleSortHeaderClick,
726
+ openDetail,
727
+ openSelectedRow,
707
728
  previewOrder,
708
729
  selectBrowseTab,
709
730
  selectCategory,
710
731
  selectMarket,
711
- selectRow,
712
732
  setDetailTab,
713
- setDetailSplitRatio,
733
+ setBrowseSelection,
714
734
  setHistoryRange,
715
735
  setHoveredIdx,
716
736
  setSearchQuery,
@@ -2,6 +2,7 @@ import type { GloomPlugin, GloomPluginContext } from "../../types/plugin";
2
2
  import { parsePredictionSearchShortcut } from "./navigation";
3
3
  import { PredictionMarketsPane } from "./pane";
4
4
  import { attachPredictionMarketsPersistence } from "./services/fetch";
5
+ import { predictionMarketsCliCommand } from "./cli";
5
6
  import {
6
7
  buildPredictionMarketsPaneSettingsDef,
7
8
  createPredictionMarketsPaneSettings,
@@ -26,6 +27,7 @@ export const predictionMarketsPlugin: GloomPlugin = {
26
27
  description:
27
28
  "Browse prediction markets (Polymarket and Kalshi).",
28
29
  toggleable: true,
30
+ cliCommands: [predictionMarketsCliCommand],
29
31
  panes: [
30
32
  {
31
33
  id: PANE_ID,
@@ -7,7 +7,7 @@ import {
7
7
  type AppConfig,
8
8
  type SavedLayout,
9
9
  } from "../../types/config";
10
- import type { PaneDef } from "../../types/plugin";
10
+ import type { CliLaunchRequest, PaneDef } from "../../types/plugin";
11
11
  import type { AppSessionSnapshot } from "../../state/session-persistence";
12
12
  import { PREDICTION_CATEGORY_OPTIONS, type PredictionCategoryId } from "./categories";
13
13
  import { BROWSE_TABS } from "./navigation";
@@ -48,20 +48,7 @@ function normalizeArg(value: string): string {
48
48
  return value.trim().toLowerCase();
49
49
  }
50
50
 
51
- export function parsePredictionLaunchArgs(
52
- args: string[],
53
- ): PredictionLaunchIntent | null {
54
- const [command, ...rest] = args;
55
- if (!command) return null;
56
- const normalizedCommand = normalizeArg(command);
57
- if (
58
- normalizedCommand !== "predictions" &&
59
- normalizedCommand !== "prediction-markets" &&
60
- normalizedCommand !== "pm"
61
- ) {
62
- return null;
63
- }
64
-
51
+ export function parsePredictionCommandArgs(args: string[]): PredictionLaunchIntent {
65
52
  let venueScope: PredictionVenueScope = "all";
66
53
  let categoryId: PredictionCategoryId = "all";
67
54
  let browseTab: PredictionBrowseTab = "top";
@@ -70,7 +57,7 @@ export function parsePredictionLaunchArgs(
70
57
  let browseExplicit = false;
71
58
  const searchTokens: string[] = [];
72
59
 
73
- for (const arg of rest) {
60
+ for (const arg of args) {
74
61
  const normalized = normalizeArg(arg);
75
62
  if (!normalized) continue;
76
63
 
@@ -100,6 +87,23 @@ export function parsePredictionLaunchArgs(
100
87
  };
101
88
  }
102
89
 
90
+ export function parsePredictionLaunchArgs(
91
+ args: string[],
92
+ ): PredictionLaunchIntent | null {
93
+ const [command, ...rest] = args;
94
+ if (!command) return null;
95
+ const normalizedCommand = normalizeArg(command);
96
+ if (
97
+ normalizedCommand !== "predictions" &&
98
+ normalizedCommand !== "prediction-markets" &&
99
+ normalizedCommand !== "pm"
100
+ ) {
101
+ return null;
102
+ }
103
+
104
+ return parsePredictionCommandArgs(rest);
105
+ }
106
+
103
107
  function syncActiveLayout(
104
108
  layouts: SavedLayout[],
105
109
  activeLayoutIndex: number,
@@ -238,3 +242,34 @@ export function applyPredictionLaunchIntentToSessionSnapshot(
238
242
  savedAt: Date.now(),
239
243
  };
240
244
  }
245
+
246
+ export function createPredictionLaunchRequest(
247
+ intent: PredictionLaunchIntent,
248
+ ): CliLaunchRequest<{ paneInstanceId: string; intent: PredictionLaunchIntent }> {
249
+ return {
250
+ applyConfig(config, env) {
251
+ const result = applyPredictionLaunchIntentToConfig(config, intent, {
252
+ width: Math.max(env.terminalWidth, 120),
253
+ height: Math.max(env.terminalHeight, 40),
254
+ });
255
+ return {
256
+ config: result.config,
257
+ launchState: {
258
+ paneInstanceId: result.paneInstanceId,
259
+ intent,
260
+ },
261
+ };
262
+ },
263
+ applySessionSnapshot(config, snapshot, launchState) {
264
+ if (!launchState) {
265
+ return applyPredictionLaunchIntentToSessionSnapshot(config, snapshot, PREDICTION_MAIN_INSTANCE_ID, intent);
266
+ }
267
+ return applyPredictionLaunchIntentToSessionSnapshot(
268
+ config,
269
+ snapshot,
270
+ launchState.paneInstanceId,
271
+ launchState.intent,
272
+ );
273
+ },
274
+ };
275
+ }
@@ -170,13 +170,17 @@ export function getNextPredictionSort(
170
170
  current: PredictionSortPreference,
171
171
  columnId: string,
172
172
  ): PredictionSortPreference {
173
+ const initialDirection =
174
+ columnId === "ends" || TEXT_SORT_COLUMNS.has(columnId) ? "asc" : "desc";
175
+ const alternateDirection = initialDirection === "asc" ? "desc" : "asc";
176
+
173
177
  if (current.columnId !== columnId) {
174
- if (columnId === "ends") return { columnId, direction: "asc" };
175
- if (TEXT_SORT_COLUMNS.has(columnId)) return { columnId, direction: "asc" };
176
- return { columnId, direction: "desc" };
178
+ return { columnId, direction: initialDirection };
179
+ }
180
+ if (current.direction === initialDirection) {
181
+ return { columnId, direction: alternateDirection };
177
182
  }
178
- if (current.direction === "desc") return { columnId, direction: "asc" };
179
- return { columnId: null, direction: "desc" };
183
+ return { columnId: null, direction: initialDirection };
180
184
  }
181
185
 
182
186
  export function filterPredictionMarkets(