gloomberb 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +45 -69
  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 +363 -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 +4 -2
  37. package/src/components/layout/header.tsx +3 -3
  38. package/src/components/layout/pane-header.tsx +26 -2
  39. package/src/components/layout/pane-sizing.ts +6 -0
  40. package/src/components/layout/pane.tsx +5 -1
  41. package/src/components/layout/shell.test.tsx +77 -5
  42. package/src/components/layout/shell.tsx +67 -52
  43. package/src/components/layout/status-bar.test.tsx +66 -0
  44. package/src/components/layout/status-bar.tsx +26 -5
  45. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  46. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  47. package/src/components/ticker-list-table.test.tsx +83 -0
  48. package/src/components/ticker-list-table.tsx +181 -97
  49. package/src/components/ui/data-table.tsx +50 -5
  50. package/src/components/ui/index.ts +1 -0
  51. package/src/components/ui/page-stack-view.tsx +60 -0
  52. package/src/components/use-double-click-activation.ts +31 -0
  53. package/src/index.tsx +10 -10
  54. package/src/market-data/coordinator.test.ts +92 -4
  55. package/src/market-data/coordinator.ts +32 -5
  56. package/src/market-data/hooks.test.tsx +120 -0
  57. package/src/market-data/hooks.tsx +53 -16
  58. package/src/market-data/request-types.ts +4 -2
  59. package/src/market-data/selectors.ts +24 -58
  60. package/src/plugins/builtin/chat-controller.test.ts +400 -3
  61. package/src/plugins/builtin/chat-controller.ts +328 -35
  62. package/src/plugins/builtin/chat.test.tsx +118 -6
  63. package/src/plugins/builtin/chat.tsx +180 -116
  64. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  65. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  66. package/src/plugins/builtin/layout-manager.tsx +9 -0
  67. package/src/plugins/builtin/news.tsx +1 -11
  68. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  69. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  70. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  71. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  72. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  73. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  74. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  75. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  76. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  77. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  78. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  79. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  80. package/src/plugins/builtin/sec.tsx +1 -11
  81. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  82. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  83. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  84. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  85. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  86. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  87. package/src/plugins/catalog.ts +65 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  89. package/src/plugins/ibkr/gateway-service.test.ts +67 -1
  90. package/src/plugins/ibkr/gateway-service.ts +147 -7
  91. package/src/plugins/ibkr/index.tsx +7 -1
  92. package/src/plugins/pane-manager.test.ts +1 -1
  93. package/src/plugins/plugin-runtime.tsx +27 -7
  94. package/src/plugins/prediction-markets/cli.ts +28 -0
  95. package/src/plugins/prediction-markets/controller-data.ts +132 -17
  96. package/src/plugins/prediction-markets/controller.ts +142 -122
  97. package/src/plugins/prediction-markets/index.tsx +7 -1
  98. package/src/plugins/prediction-markets/launch.ts +51 -16
  99. package/src/plugins/prediction-markets/metrics.ts +9 -5
  100. package/src/plugins/prediction-markets/pane.test.tsx +197 -57
  101. package/src/plugins/prediction-markets/pane.tsx +80 -155
  102. package/src/plugins/prediction-markets/services/fetch.ts +4 -0
  103. package/src/plugins/prediction-markets/table.tsx +6 -3
  104. package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/plugins/registry.ts +23 -0
  107. package/src/sources/gloomberb-cloud.test.ts +29 -0
  108. package/src/sources/gloomberb-cloud.ts +89 -1
  109. package/src/sources/provider-router.test.ts +265 -3
  110. package/src/sources/provider-router.ts +239 -11
  111. package/src/sources/yahoo-finance.test.ts +26 -0
  112. package/src/sources/yahoo-finance.ts +54 -3
  113. package/src/state/app-context-selector.test.tsx +99 -0
  114. package/src/state/app-context.test.ts +18 -0
  115. package/src/state/app-context.tsx +170 -65
  116. package/src/state/selectors.ts +25 -0
  117. package/src/theme/colors.ts +8 -8
  118. package/src/theme/themes.test.ts +2 -0
  119. package/src/theme/themes.ts +22 -10
  120. package/src/types/broker.ts +21 -0
  121. package/src/types/config.ts +17 -1
  122. package/src/types/data-provider.ts +18 -0
  123. package/src/types/financials.ts +31 -1
  124. package/src/types/plugin.ts +70 -0
  125. package/src/updater.test.ts +2 -0
  126. package/src/utils/api-client.test.ts +57 -0
  127. package/src/utils/api-client.ts +47 -12
  128. package/src/utils/back-navigation.ts +27 -0
  129. package/src/utils/format.test.ts +14 -0
  130. package/src/utils/format.ts +3 -3
  131. package/src/utils/market-status.ts +46 -1
  132. package/src/utils/quote-freshness.ts +127 -0
  133. package/src/utils/quote-resolution.test.ts +277 -0
  134. package/src/utils/quote-resolution.ts +641 -0
  135. package/src/utils/slugify.ts +3 -0
  136. package/src/utils/timestamp.ts +25 -0
  137. package/src/version.ts +1 -1
  138. package/src/cli/commands/portfolio.ts +0 -210
  139. package/src/cli/commands/watchlist.ts +0 -210
  140. package/src/plugins/builtin/data-providers.ts +0 -16
  141. package/src/sources/exchange-rates.ts +0 -18
  142. package/src/utils/ascii-chart.ts +0 -172
  143. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -83,7 +83,58 @@ describe("prediction markets pane interactions", () => {
83
83
  expect(frame).toContain("Will the Fed cut rates?");
84
84
  });
85
85
 
86
- test("renders the merged market browser and supports venue switching", async () => {
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
+
137
+ test("selects a market on single click and opens detail on double click", async () => {
87
138
  installPredictionMarketMocks();
88
139
 
89
140
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -115,9 +166,45 @@ describe("prediction markets pane interactions", () => {
115
166
 
116
167
  frame = testSetup.captureCharFrame();
117
168
  expect(frame).toContain("Will the Fed cut rates?");
169
+ expect(frame).not.toContain("Kalshi primary rule");
170
+
171
+ await act(async () => {
172
+ await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
173
+ await testSetup!.renderOnce();
174
+ });
175
+ await flushFrames(testSetup);
176
+
177
+ frame = testSetup.captureCharFrame();
118
178
  expect(frame).toContain("Kalshi primary rule");
119
179
  });
120
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
+
121
208
  test("loads selected detail once instead of refetching in a render loop", async () => {
122
209
  const { fetchUrls } = installPredictionMarketMocks();
123
210
 
@@ -134,6 +221,8 @@ describe("prediction markets pane interactions", () => {
134
221
  await act(async () => {
135
222
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
136
223
  await testSetup!.renderOnce();
224
+ await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
225
+ await testSetup!.renderOnce();
137
226
  });
138
227
  await flushFrames(testSetup, 8);
139
228
 
@@ -157,7 +246,7 @@ describe("prediction markets pane interactions", () => {
157
246
  expect(testSetup.captureCharFrame()).not.toContain("Loading market detail...");
158
247
  });
159
248
 
160
- test("moves selection through the list with keyboard navigation", async () => {
249
+ test("moves selection through the list with keyboard navigation without opening detail", async () => {
161
250
  installPredictionMarketMocks();
162
251
 
163
252
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -176,6 +265,102 @@ describe("prediction markets pane interactions", () => {
176
265
 
177
266
  const frame = testSetup.captureCharFrame();
178
267
  expect(frame).toContain("Will the Fed cut rates?");
268
+ expect(frame).not.toContain("Kalshi primary rule");
269
+ expect(frame).not.toContain("<- Back");
270
+ });
271
+
272
+ test("keeps a clicked header sort instead of snapping back to the default sort", async () => {
273
+ installPredictionMarketMocks();
274
+
275
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
276
+ await flushFrames(testSetup);
277
+
278
+ const frame = testSetup.captureCharFrame();
279
+ const lines = frame.split("\n");
280
+ const headerRow = lines.findIndex((line) => line.includes("VENUE"));
281
+ const headerCol = lines[headerRow]?.indexOf("VENUE") ?? -1;
282
+
283
+ expect(headerRow).toBeGreaterThan(-1);
284
+ expect(headerCol).toBeGreaterThan(-1);
285
+
286
+ await act(async () => {
287
+ await testSetup!.mockMouse.click(headerCol + 1, headerRow);
288
+ await testSetup!.renderOnce();
289
+ });
290
+ await flushFrames(testSetup, 4);
291
+
292
+ const nextFrame = testSetup.captureCharFrame();
293
+ const nextLines = nextFrame.split("\n");
294
+ const kalshiRow = nextLines.findIndex((line) =>
295
+ line.includes("Will the Fed cut rates?"),
296
+ );
297
+ const polymarketRow = nextLines.findIndex((line) =>
298
+ line.includes("Will inflation fall?"),
299
+ );
300
+
301
+ expect(
302
+ harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
303
+ "prediction-markets"
304
+ ]?.sortPreference,
305
+ ).toEqual({ columnId: "venue", direction: "asc" });
306
+ expect(nextLines[headerRow]).toContain("VENUE ▲");
307
+ expect(kalshiRow).toBeGreaterThan(-1);
308
+ expect(polymarketRow).toBeGreaterThan(-1);
309
+ expect(kalshiRow).toBeLessThan(polymarketRow);
310
+ });
311
+
312
+ test("cycles the ends header through reverse date order before resetting", async () => {
313
+ installPredictionMarketMocks();
314
+
315
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
316
+ await flushFrames(testSetup);
317
+
318
+ const frame = testSetup.captureCharFrame();
319
+ const lines = frame.split("\n");
320
+ const headerRow = lines.findIndex((line) => line.includes("ENDS"));
321
+ const headerCol = lines[headerRow]?.indexOf("ENDS") ?? -1;
322
+
323
+ expect(headerRow).toBeGreaterThan(-1);
324
+ expect(headerCol).toBeGreaterThan(-1);
325
+
326
+ await act(async () => {
327
+ await testSetup!.mockMouse.click(headerCol + 1, headerRow);
328
+ await testSetup!.renderOnce();
329
+ });
330
+ await flushFrames(testSetup, 2);
331
+
332
+ const sortedFrame = testSetup.captureCharFrame();
333
+ const sortedLines = sortedFrame.split("\n");
334
+ const sortedHeaderRow = sortedLines.findIndex((line) => line.includes("ENDS"));
335
+ const sortedHeaderCol = sortedLines[sortedHeaderRow]?.indexOf("ENDS") ?? -1;
336
+
337
+ expect(sortedHeaderRow).toBeGreaterThan(-1);
338
+ expect(sortedHeaderCol).toBeGreaterThan(-1);
339
+
340
+ await act(async () => {
341
+ await testSetup!.mockMouse.click(sortedHeaderCol + 1, sortedHeaderRow);
342
+ await testSetup!.renderOnce();
343
+ });
344
+ await flushFrames(testSetup, 4);
345
+
346
+ const nextFrame = testSetup.captureCharFrame();
347
+ const nextLines = nextFrame.split("\n");
348
+ const kalshiRow = nextLines.findIndex((line) =>
349
+ line.includes("Will the Fed cut rates?"),
350
+ );
351
+ const polymarketRow = nextLines.findIndex((line) =>
352
+ line.includes("Will inflation fall?"),
353
+ );
354
+
355
+ expect(
356
+ harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
357
+ "prediction-markets"
358
+ ]?.sortPreference,
359
+ ).toEqual({ columnId: "ends", direction: "desc" });
360
+ expect(nextLines[headerRow]).toContain("ENDS ▼");
361
+ expect(kalshiRow).toBeGreaterThan(-1);
362
+ expect(polymarketRow).toBeGreaterThan(-1);
363
+ expect(kalshiRow).toBeLessThan(polymarketRow);
179
364
  });
180
365
 
181
366
  test("does not wrap to the last row when moving up without a selection", async () => {
@@ -197,7 +382,7 @@ describe("prediction markets pane interactions", () => {
197
382
  ).toBe("polymarket:pm-1");
198
383
  });
199
384
 
200
- test("supports detail outcome navigation and escape focus return from the keyboard", async () => {
385
+ test("supports detail outcome navigation and backspace return from the keyboard", async () => {
201
386
  attachPredictionMarketsPersistence(new MemoryPersistence());
202
387
 
203
388
  globalThis.fetch = (async (input: Request | string | URL) => {
@@ -326,17 +511,15 @@ describe("prediction markets pane interactions", () => {
326
511
  ]?.selectedDetailMarketKey,
327
512
  ).toBe("kalshi:KXFED-27APR-T4.50");
328
513
 
329
- await emitKeypress(testSetup, { name: "esc", sequence: "\u001b" });
514
+ await emitKeypress(testSetup, { name: "backspace", sequence: "\u007f" });
330
515
  await flushFrames(testSetup);
331
516
 
332
- expect(
333
- harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
334
- "prediction-markets"
335
- ]?.focusRegion,
336
- ).toBe("list");
517
+ const frame = testSetup.captureCharFrame();
518
+ expect(frame).toContain("search markets");
519
+ expect(frame).not.toContain("Rule 2");
337
520
  });
338
521
 
339
- test("starts with no detail pane and toggles the detail pane off on repeat selection", async () => {
522
+ test("opens full-page detail from a row double click and preserves the browse selection", async () => {
340
523
  installPredictionMarketMocks();
341
524
 
342
525
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -352,62 +535,19 @@ describe("prediction markets pane interactions", () => {
352
535
  await act(async () => {
353
536
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
354
537
  await testSetup!.renderOnce();
355
- });
356
- await flushFrames(testSetup);
357
-
358
- await act(async () => {
359
538
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
360
539
  await testSetup!.renderOnce();
361
540
  });
362
541
  await flushFrames(testSetup);
363
542
 
543
+ const detailFrame = testSetup.captureCharFrame();
544
+ expect(detailFrame).toContain("<- Back");
545
+ expect(detailFrame).toContain("Kalshi primary rule");
364
546
  expect(
365
547
  harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
366
548
  "prediction-markets"
367
549
  ]?.selectedRowKey,
368
- ).toBeNull();
369
- });
370
-
371
- test("resizes the list/detail split by dragging the divider", async () => {
372
- installPredictionMarketMocks();
373
-
374
- testSetup = await testRender(<Harness />, { width: 120, height: 34 });
375
- await flushFrames(testSetup);
376
-
377
- let frame = testSetup.captureCharFrame();
378
- const lines = frame.split("\n");
379
- const kalshiRow = lines.findIndex((line) =>
380
- line.includes("Will the Fed cut rates?"),
381
- );
382
- const kalshiCol = lines[kalshiRow]?.indexOf("Will the Fed cut rates?") ?? -1;
383
-
384
- await act(async () => {
385
- await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
386
- await testSetup!.renderOnce();
387
- });
388
- await flushFrames(testSetup);
389
-
390
- frame = testSetup.captureCharFrame();
391
- const dividerLines = frame.split("\n");
392
- const dividerRow = dividerLines.findIndex((line) => line.includes("│"));
393
- const dividerCol = dividerLines[dividerRow]?.indexOf("│") ?? -1;
394
-
395
- await act(async () => {
396
- await testSetup!.mockMouse.drag(
397
- dividerCol,
398
- dividerRow,
399
- dividerCol + 10,
400
- dividerRow,
401
- );
402
- await testSetup!.renderOnce();
403
- });
404
- await flushFrames(testSetup);
405
-
406
- expect(
407
- harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
408
- "prediction-markets"
409
- ]?.detailSplitRatio,
410
- ).toBeGreaterThan(0.42);
550
+ ).toBe("kalshi:KAL-1");
411
551
  });
412
552
 
413
553
  test("switches categories with arrows and venues with shift+arrows", async () => {
@@ -1,106 +1,29 @@
1
- import { useRef } from "react";
2
- import { TextAttributes, type BoxRenderable } from "@opentui/core";
3
- import { TabBar } from "../../components";
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { PageStackView, TabBar } from "../../components";
3
+ import type { PaneProps } from "../../types/plugin";
4
+ import { colors } from "../../theme/colors";
5
+ import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
4
6
  import { usePredictionMarketsController } from "./controller";
5
7
  import { PredictionMarketDetailPane } from "./detail/pane";
6
8
  import { BROWSE_TABS, VENUE_TABS } from "./navigation";
7
9
  import { PredictionMarketsTable } from "./table";
8
- import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
9
10
  import type { PredictionBrowseTab, PredictionCategoryId } from "./types";
10
- import type { PaneProps } from "../../types/plugin";
11
- import { colors } from "../../theme/colors";
12
-
13
- function clamp(value: number, min: number, max: number): number {
14
- return Math.min(Math.max(value, min), max);
15
- }
16
11
 
17
12
  export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
18
13
  const controller = usePredictionMarketsController({ focused });
19
- const rootRef = useRef<BoxRenderable>(null);
20
- const splitDragRef = useRef<{ startX: number; startRatio: number } | null>(
21
- null,
22
- );
23
- const minTableWidth = 44;
24
- const minDetailWidth = 36;
25
- const effectiveMinTableWidth = controller.selectedSummary
26
- ? Math.min(minTableWidth, Math.max(width - minDetailWidth - 1, 20))
27
- : width;
28
- const effectiveMinDetailWidth = controller.selectedSummary
29
- ? Math.min(minDetailWidth, Math.max(width - effectiveMinTableWidth - 1, 20))
30
- : 0;
31
- const listWidth = controller.selectedSummary
32
- ? clamp(
33
- Math.round(width * controller.detailSplitRatio),
34
- effectiveMinTableWidth,
35
- Math.max(effectiveMinTableWidth, width - effectiveMinDetailWidth - 1),
36
- )
37
- : width;
38
- const detailWidth = controller.selectedSummary
39
- ? Math.max(width - listWidth - 1, effectiveMinDetailWidth)
40
- : 0;
41
- const dividerColor =
42
- controller.focusRegion === "detail" ? colors.borderFocused : colors.border;
43
-
44
- const handlePaneMouse = (event: {
45
- type?: string;
46
- x?: number;
47
- preventDefault?: () => void;
48
- stopPropagation?: () => void;
49
- }) => {
50
- if (!controller.selectedSummary || !rootRef.current) {
51
- splitDragRef.current = null;
52
- return;
53
- }
54
-
55
- const localX = (event.x ?? 0) - rootRef.current.x;
56
- const dividerX = listWidth;
57
-
58
- if (
59
- event.type === "down" &&
60
- localX >= dividerX &&
61
- localX <= dividerX + 1
62
- ) {
63
- splitDragRef.current = {
64
- startX: localX,
65
- startRatio: controller.detailSplitRatio,
66
- };
67
- event.preventDefault?.();
68
- event.stopPropagation?.();
69
- return;
70
- }
14
+ const catalogStatusColor =
15
+ controller.catalogStatus?.tone === "danger"
16
+ ? colors.negative
17
+ : colors.borderFocused;
71
18
 
72
- if (!splitDragRef.current) return;
73
-
74
- if (event.type === "drag") {
75
- const delta = localX - splitDragRef.current.startX;
76
- const nextListWidth = clamp(
77
- Math.round(width * splitDragRef.current.startRatio) + delta,
78
- effectiveMinTableWidth,
79
- Math.max(effectiveMinTableWidth, width - effectiveMinDetailWidth - 1),
80
- );
81
- controller.actions.setDetailSplitRatio(nextListWidth / Math.max(width, 1));
82
- event.preventDefault?.();
83
- event.stopPropagation?.();
84
- return;
85
- }
86
-
87
- if (event.type === "up" || event.type === "drag-end") {
88
- splitDragRef.current = null;
89
- event.preventDefault?.();
90
- event.stopPropagation?.();
91
- }
92
- };
93
-
94
- return (
19
+ const browseContent = (
95
20
  <box
96
- ref={rootRef}
97
21
  flexDirection="column"
98
22
  width={width}
99
23
  height={height}
100
24
  backgroundColor={colors.panel}
101
- onMouse={handlePaneMouse}
102
25
  >
103
- {!controller.paneSettings.hideTabs && (
26
+ {!controller.paneSettings.hideTabs ? (
104
27
  <TabBar
105
28
  tabs={VENUE_TABS.map((tab) => ({
106
29
  label: tab.label,
@@ -110,7 +33,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
110
33
  onSelect={controller.actions.setVenue}
111
34
  compact
112
35
  />
113
- )}
36
+ ) : null}
114
37
 
115
38
  <box flexDirection="row" height={1} paddingX={1} gap={2}>
116
39
  <box
@@ -118,9 +41,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
118
41
  onMouseDown={controller.actions.focusSearch}
119
42
  width={Math.max(18, Math.floor(width * 0.32))}
120
43
  >
121
- <text fg={colors.textDim}>
122
- {controller.searchFocused ? "?" : "/"}
123
- </text>
44
+ <text fg={colors.textDim}>{controller.searchFocused ? "?" : "/"}</text>
124
45
  <box width={1} />
125
46
  {controller.searchFocused ? (
126
47
  <input
@@ -168,7 +89,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
168
89
  </box>
169
90
  </box>
170
91
 
171
- {PREDICTION_CATEGORY_OPTIONS.length > 1 && (
92
+ {PREDICTION_CATEGORY_OPTIONS.length > 1 ? (
172
93
  <scrollbox height={1} scrollX focusable={false}>
173
94
  <box flexDirection="row" paddingX={1} gap={2}>
174
95
  {PREDICTION_CATEGORY_OPTIONS.map((category) => {
@@ -194,71 +115,75 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
194
115
  })}
195
116
  </box>
196
117
  </scrollbox>
197
- )}
118
+ ) : null}
198
119
 
199
- {controller.catalogError && (
200
- <box height={1} paddingX={1}>
201
- <text fg={colors.negative}>{controller.catalogError}</text>
120
+ {controller.catalogStatus ? (
121
+ <box height={1} paddingX={1} width={width} backgroundColor={colors.panel}>
122
+ <text fg={catalogStatusColor}>{controller.catalogStatus.message}</text>
202
123
  </box>
203
- )}
204
-
205
- <box flexDirection="row" flexGrow={1}>
206
- <box
207
- width={listWidth}
208
- flexDirection="column"
209
- onMouseDown={controller.actions.focusList}
210
- >
211
- <PredictionMarketsTable
212
- columns={controller.visibleColumns}
213
- rows={controller.visibleRows}
214
- selectedRowKey={controller.selectedRow?.key ?? null}
215
- hoveredIdx={controller.hoveredIdx}
216
- setHoveredIdx={controller.actions.setHoveredIdx}
217
- setSelectedRowKey={controller.actions.selectRow}
218
- watchlist={controller.watchlistSet}
219
- onToggleWatchlist={controller.actions.toggleWatchlist}
220
- sortPreference={controller.sortPreference}
221
- onHeaderClick={controller.actions.handleSortHeaderClick}
222
- headerScrollRef={controller.headerScrollRef}
223
- scrollRef={controller.scrollRef}
224
- syncHeaderScroll={controller.layout.syncHeaderScroll}
225
- onBodyScrollActivity={controller.layout.onBodyScrollActivity}
226
- />
227
- </box>
228
-
229
- {controller.selectedSummary && (
230
- <>
231
- <box width={1}>
232
- <text fg={dividerColor}>│</text>
233
- </box>
234
-
235
- <box
236
- width={detailWidth}
237
- flexDirection="column"
238
- paddingX={1}
239
- onMouseDown={controller.actions.focusDetail}
240
- >
241
- <PredictionMarketDetailPane
242
- detail={controller.detail}
243
- detailError={controller.detailError}
244
- detailLoadCount={controller.detailLoadCount}
245
- detailTab={controller.detailTab}
246
- detailWidth={Math.max(detailWidth - 2, 24)}
247
- focused={controller.focusRegion === "detail"}
248
- height={height}
249
- historyRange={controller.historyRange}
250
- onDetailTabChange={controller.actions.setDetailTab}
251
- onHistoryRangeChange={controller.actions.setHistoryRange}
252
- onPreviewOrder={controller.actions.previewOrder}
253
- onSelectMarket={controller.actions.selectMarket}
254
- scrollRef={controller.detailScrollRef}
255
- selectedRow={controller.selectedRow}
256
- selectedSummary={controller.selectedSummary}
257
- />
258
- </box>
259
- </>
260
- )}
124
+ ) : null}
125
+
126
+ <box flexDirection="column" flexGrow={1}>
127
+ <PredictionMarketsTable
128
+ columns={controller.visibleColumns}
129
+ rows={controller.visibleRows}
130
+ selectedRowKey={controller.selectedRow?.key ?? null}
131
+ hoveredIdx={controller.hoveredIdx}
132
+ setHoveredIdx={controller.actions.setHoveredIdx}
133
+ onSelectRow={(rowKey) => controller.actions.setBrowseSelection(rowKey)}
134
+ onOpenRow={controller.actions.openSelectedRow}
135
+ watchlist={controller.watchlistSet}
136
+ onToggleWatchlist={controller.actions.toggleWatchlist}
137
+ sortPreference={controller.sortPreference}
138
+ onHeaderClick={controller.actions.handleSortHeaderClick}
139
+ headerScrollRef={controller.headerScrollRef}
140
+ scrollRef={controller.scrollRef}
141
+ syncHeaderScroll={controller.layout.syncHeaderScroll}
142
+ onBodyScrollActivity={controller.layout.onBodyScrollActivity}
143
+ />
261
144
  </box>
262
145
  </box>
263
146
  );
147
+
148
+ const detailContent =
149
+ controller.selectedSummary && controller.selectedRow ? (
150
+ <box
151
+ flexDirection="column"
152
+ flexGrow={1}
153
+ width={width}
154
+ height={Math.max(height - 1, 1)}
155
+ paddingX={1}
156
+ backgroundColor={colors.panel}
157
+ >
158
+ <PredictionMarketDetailPane
159
+ detail={controller.detail}
160
+ detailError={controller.detailError}
161
+ detailLoadCount={controller.detailLoadCount}
162
+ detailTab={controller.detailTab}
163
+ detailWidth={Math.max(width - 2, 24)}
164
+ focused={focused && controller.detailOpen}
165
+ height={Math.max(height - 1, 1)}
166
+ historyRange={controller.historyRange}
167
+ onDetailTabChange={controller.actions.setDetailTab}
168
+ onHistoryRangeChange={controller.actions.setHistoryRange}
169
+ onPreviewOrder={controller.actions.previewOrder}
170
+ onSelectMarket={controller.actions.selectMarket}
171
+ scrollRef={controller.detailScrollRef}
172
+ selectedRow={controller.selectedRow}
173
+ selectedSummary={controller.selectedSummary}
174
+ />
175
+ </box>
176
+ ) : (
177
+ <box flexGrow={1} backgroundColor={colors.panel} />
178
+ );
179
+
180
+ return (
181
+ <PageStackView
182
+ focused={focused}
183
+ detailOpen={controller.detailOpen && !!controller.selectedSummary}
184
+ onBack={controller.actions.closeDetail}
185
+ rootContent={browseContent}
186
+ detailContent={detailContent}
187
+ />
188
+ );
264
189
  }
@@ -19,6 +19,10 @@ export function attachPredictionMarketsPersistence(
19
19
  predictionMarketsPersistence = persistence;
20
20
  }
21
21
 
22
+ export function resetPredictionMarketsPersistence(): void {
23
+ predictionMarketsPersistence = null;
24
+ }
25
+
22
26
  export function getPredictionMarketsPersistence(): PluginPersistence | null {
23
27
  return predictionMarketsPersistence;
24
28
  }
@@ -14,7 +14,8 @@ export function PredictionMarketsTable({
14
14
  selectedRowKey,
15
15
  hoveredIdx,
16
16
  setHoveredIdx,
17
- setSelectedRowKey,
17
+ onSelectRow,
18
+ onOpenRow,
18
19
  watchlist,
19
20
  onToggleWatchlist,
20
21
  sortPreference,
@@ -29,7 +30,8 @@ export function PredictionMarketsTable({
29
30
  selectedRowKey: string | null;
30
31
  hoveredIdx: number | null;
31
32
  setHoveredIdx: (index: number | null) => void;
32
- setSelectedRowKey: (rowKey: string) => void;
33
+ onSelectRow: (rowKey: string) => void;
34
+ onOpenRow: (rowKey: string) => void;
33
35
  watchlist: Set<string>;
34
36
  onToggleWatchlist: (row: PredictionListRow) => void;
35
37
  sortPreference: PredictionSortPreference;
@@ -56,7 +58,8 @@ export function PredictionMarketsTable({
56
58
  setHoveredIdx={setHoveredIdx}
57
59
  getItemKey={(row) => row.key}
58
60
  isSelected={(row) => selectedRowKey === row.key}
59
- onSelect={(row) => setSelectedRowKey(row.key)}
61
+ onSelect={(row) => onSelectRow(row.key)}
62
+ onActivate={(row) => onOpenRow(row.key)}
60
63
  virtualize
61
64
  renderCell={(row, column, _index, rowState) => {
62
65
  const watchlisted = row.watchMarketKeys.some((marketKey) =>