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
@@ -83,7 +83,7 @@ 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("selects a market on single click and opens detail on double click", async () => {
87
87
  installPredictionMarketMocks();
88
88
 
89
89
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -115,6 +115,15 @@ describe("prediction markets pane interactions", () => {
115
115
 
116
116
  frame = testSetup.captureCharFrame();
117
117
  expect(frame).toContain("Will the Fed cut rates?");
118
+ expect(frame).not.toContain("Kalshi primary rule");
119
+
120
+ await act(async () => {
121
+ await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
122
+ await testSetup!.renderOnce();
123
+ });
124
+ await flushFrames(testSetup);
125
+
126
+ frame = testSetup.captureCharFrame();
118
127
  expect(frame).toContain("Kalshi primary rule");
119
128
  });
120
129
 
@@ -134,6 +143,8 @@ describe("prediction markets pane interactions", () => {
134
143
  await act(async () => {
135
144
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
136
145
  await testSetup!.renderOnce();
146
+ await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
147
+ await testSetup!.renderOnce();
137
148
  });
138
149
  await flushFrames(testSetup, 8);
139
150
 
@@ -157,7 +168,7 @@ describe("prediction markets pane interactions", () => {
157
168
  expect(testSetup.captureCharFrame()).not.toContain("Loading market detail...");
158
169
  });
159
170
 
160
- test("moves selection through the list with keyboard navigation", async () => {
171
+ test("moves selection through the list with keyboard navigation without opening detail", async () => {
161
172
  installPredictionMarketMocks();
162
173
 
163
174
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -176,6 +187,102 @@ describe("prediction markets pane interactions", () => {
176
187
 
177
188
  const frame = testSetup.captureCharFrame();
178
189
  expect(frame).toContain("Will the Fed cut rates?");
190
+ expect(frame).not.toContain("Kalshi primary rule");
191
+ expect(frame).not.toContain("<- Back");
192
+ });
193
+
194
+ test("keeps a clicked header sort instead of snapping back to the default sort", async () => {
195
+ installPredictionMarketMocks();
196
+
197
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
198
+ await flushFrames(testSetup);
199
+
200
+ const frame = testSetup.captureCharFrame();
201
+ const lines = frame.split("\n");
202
+ const headerRow = lines.findIndex((line) => line.includes("VENUE"));
203
+ const headerCol = lines[headerRow]?.indexOf("VENUE") ?? -1;
204
+
205
+ expect(headerRow).toBeGreaterThan(-1);
206
+ expect(headerCol).toBeGreaterThan(-1);
207
+
208
+ await act(async () => {
209
+ await testSetup!.mockMouse.click(headerCol + 1, headerRow);
210
+ await testSetup!.renderOnce();
211
+ });
212
+ await flushFrames(testSetup, 4);
213
+
214
+ const nextFrame = testSetup.captureCharFrame();
215
+ const nextLines = nextFrame.split("\n");
216
+ const kalshiRow = nextLines.findIndex((line) =>
217
+ line.includes("Will the Fed cut rates?"),
218
+ );
219
+ const polymarketRow = nextLines.findIndex((line) =>
220
+ line.includes("Will inflation fall?"),
221
+ );
222
+
223
+ expect(
224
+ harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
225
+ "prediction-markets"
226
+ ]?.sortPreference,
227
+ ).toEqual({ columnId: "venue", direction: "asc" });
228
+ expect(nextLines[headerRow]).toContain("VENUE ▲");
229
+ expect(kalshiRow).toBeGreaterThan(-1);
230
+ expect(polymarketRow).toBeGreaterThan(-1);
231
+ expect(kalshiRow).toBeLessThan(polymarketRow);
232
+ });
233
+
234
+ test("cycles the ends header through reverse date order before resetting", async () => {
235
+ installPredictionMarketMocks();
236
+
237
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
238
+ await flushFrames(testSetup);
239
+
240
+ const frame = testSetup.captureCharFrame();
241
+ const lines = frame.split("\n");
242
+ const headerRow = lines.findIndex((line) => line.includes("ENDS"));
243
+ const headerCol = lines[headerRow]?.indexOf("ENDS") ?? -1;
244
+
245
+ expect(headerRow).toBeGreaterThan(-1);
246
+ expect(headerCol).toBeGreaterThan(-1);
247
+
248
+ await act(async () => {
249
+ await testSetup!.mockMouse.click(headerCol + 1, headerRow);
250
+ await testSetup!.renderOnce();
251
+ });
252
+ await flushFrames(testSetup, 2);
253
+
254
+ const sortedFrame = testSetup.captureCharFrame();
255
+ const sortedLines = sortedFrame.split("\n");
256
+ const sortedHeaderRow = sortedLines.findIndex((line) => line.includes("ENDS"));
257
+ const sortedHeaderCol = sortedLines[sortedHeaderRow]?.indexOf("ENDS") ?? -1;
258
+
259
+ expect(sortedHeaderRow).toBeGreaterThan(-1);
260
+ expect(sortedHeaderCol).toBeGreaterThan(-1);
261
+
262
+ await act(async () => {
263
+ await testSetup!.mockMouse.click(sortedHeaderCol + 1, sortedHeaderRow);
264
+ await testSetup!.renderOnce();
265
+ });
266
+ await flushFrames(testSetup, 4);
267
+
268
+ const nextFrame = testSetup.captureCharFrame();
269
+ const nextLines = nextFrame.split("\n");
270
+ const kalshiRow = nextLines.findIndex((line) =>
271
+ line.includes("Will the Fed cut rates?"),
272
+ );
273
+ const polymarketRow = nextLines.findIndex((line) =>
274
+ line.includes("Will inflation fall?"),
275
+ );
276
+
277
+ expect(
278
+ harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
279
+ "prediction-markets"
280
+ ]?.sortPreference,
281
+ ).toEqual({ columnId: "ends", direction: "desc" });
282
+ expect(nextLines[headerRow]).toContain("ENDS ▼");
283
+ expect(kalshiRow).toBeGreaterThan(-1);
284
+ expect(polymarketRow).toBeGreaterThan(-1);
285
+ expect(kalshiRow).toBeLessThan(polymarketRow);
179
286
  });
180
287
 
181
288
  test("does not wrap to the last row when moving up without a selection", async () => {
@@ -197,7 +304,7 @@ describe("prediction markets pane interactions", () => {
197
304
  ).toBe("polymarket:pm-1");
198
305
  });
199
306
 
200
- test("supports detail outcome navigation and escape focus return from the keyboard", async () => {
307
+ test("supports detail outcome navigation and backspace return from the keyboard", async () => {
201
308
  attachPredictionMarketsPersistence(new MemoryPersistence());
202
309
 
203
310
  globalThis.fetch = (async (input: Request | string | URL) => {
@@ -326,17 +433,15 @@ describe("prediction markets pane interactions", () => {
326
433
  ]?.selectedDetailMarketKey,
327
434
  ).toBe("kalshi:KXFED-27APR-T4.50");
328
435
 
329
- await emitKeypress(testSetup, { name: "esc", sequence: "\u001b" });
436
+ await emitKeypress(testSetup, { name: "backspace", sequence: "\u007f" });
330
437
  await flushFrames(testSetup);
331
438
 
332
- expect(
333
- harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
334
- "prediction-markets"
335
- ]?.focusRegion,
336
- ).toBe("list");
439
+ const frame = testSetup.captureCharFrame();
440
+ expect(frame).toContain("search markets");
441
+ expect(frame).not.toContain("Rule 2");
337
442
  });
338
443
 
339
- test("starts with no detail pane and toggles the detail pane off on repeat selection", async () => {
444
+ test("opens full-page detail from a row double click and preserves the browse selection", async () => {
340
445
  installPredictionMarketMocks();
341
446
 
342
447
  testSetup = await testRender(<Harness />, { width: 120, height: 34 });
@@ -352,62 +457,19 @@ describe("prediction markets pane interactions", () => {
352
457
  await act(async () => {
353
458
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
354
459
  await testSetup!.renderOnce();
355
- });
356
- await flushFrames(testSetup);
357
-
358
- await act(async () => {
359
460
  await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
360
461
  await testSetup!.renderOnce();
361
462
  });
362
463
  await flushFrames(testSetup);
363
464
 
465
+ const detailFrame = testSetup.captureCharFrame();
466
+ expect(detailFrame).toContain("<- Back");
467
+ expect(detailFrame).toContain("Kalshi primary rule");
364
468
  expect(
365
469
  harnessStateRef.current?.paneState[TEST_PANE_ID]?.pluginState?.[
366
470
  "prediction-markets"
367
471
  ]?.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);
472
+ ).toBe("kalshi:KAL-1");
411
473
  });
412
474
 
413
475
  test("switches categories with arrows and venues with shift+arrows", async () => {
@@ -1,106 +1,25 @@
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
- }
71
14
 
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 (
15
+ const browseContent = (
95
16
  <box
96
- ref={rootRef}
97
17
  flexDirection="column"
98
18
  width={width}
99
19
  height={height}
100
20
  backgroundColor={colors.panel}
101
- onMouse={handlePaneMouse}
102
21
  >
103
- {!controller.paneSettings.hideTabs && (
22
+ {!controller.paneSettings.hideTabs ? (
104
23
  <TabBar
105
24
  tabs={VENUE_TABS.map((tab) => ({
106
25
  label: tab.label,
@@ -110,7 +29,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
110
29
  onSelect={controller.actions.setVenue}
111
30
  compact
112
31
  />
113
- )}
32
+ ) : null}
114
33
 
115
34
  <box flexDirection="row" height={1} paddingX={1} gap={2}>
116
35
  <box
@@ -118,9 +37,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
118
37
  onMouseDown={controller.actions.focusSearch}
119
38
  width={Math.max(18, Math.floor(width * 0.32))}
120
39
  >
121
- <text fg={colors.textDim}>
122
- {controller.searchFocused ? "?" : "/"}
123
- </text>
40
+ <text fg={colors.textDim}>{controller.searchFocused ? "?" : "/"}</text>
124
41
  <box width={1} />
125
42
  {controller.searchFocused ? (
126
43
  <input
@@ -168,7 +85,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
168
85
  </box>
169
86
  </box>
170
87
 
171
- {PREDICTION_CATEGORY_OPTIONS.length > 1 && (
88
+ {PREDICTION_CATEGORY_OPTIONS.length > 1 ? (
172
89
  <scrollbox height={1} scrollX focusable={false}>
173
90
  <box flexDirection="row" paddingX={1} gap={2}>
174
91
  {PREDICTION_CATEGORY_OPTIONS.map((category) => {
@@ -194,71 +111,75 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
194
111
  })}
195
112
  </box>
196
113
  </scrollbox>
197
- )}
114
+ ) : null}
198
115
 
199
- {controller.catalogError && (
116
+ {controller.catalogError ? (
200
117
  <box height={1} paddingX={1}>
201
118
  <text fg={colors.negative}>{controller.catalogError}</text>
202
119
  </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
- )}
120
+ ) : null}
121
+
122
+ <box flexDirection="column" flexGrow={1}>
123
+ <PredictionMarketsTable
124
+ columns={controller.visibleColumns}
125
+ rows={controller.visibleRows}
126
+ selectedRowKey={controller.selectedRow?.key ?? null}
127
+ hoveredIdx={controller.hoveredIdx}
128
+ setHoveredIdx={controller.actions.setHoveredIdx}
129
+ onSelectRow={(rowKey) => controller.actions.setBrowseSelection(rowKey)}
130
+ onOpenRow={controller.actions.openSelectedRow}
131
+ watchlist={controller.watchlistSet}
132
+ onToggleWatchlist={controller.actions.toggleWatchlist}
133
+ sortPreference={controller.sortPreference}
134
+ onHeaderClick={controller.actions.handleSortHeaderClick}
135
+ headerScrollRef={controller.headerScrollRef}
136
+ scrollRef={controller.scrollRef}
137
+ syncHeaderScroll={controller.layout.syncHeaderScroll}
138
+ onBodyScrollActivity={controller.layout.onBodyScrollActivity}
139
+ />
261
140
  </box>
262
141
  </box>
263
142
  );
143
+
144
+ const detailContent =
145
+ controller.selectedSummary && controller.selectedRow ? (
146
+ <box
147
+ flexDirection="column"
148
+ flexGrow={1}
149
+ width={width}
150
+ height={Math.max(height - 1, 1)}
151
+ paddingX={1}
152
+ backgroundColor={colors.panel}
153
+ >
154
+ <PredictionMarketDetailPane
155
+ detail={controller.detail}
156
+ detailError={controller.detailError}
157
+ detailLoadCount={controller.detailLoadCount}
158
+ detailTab={controller.detailTab}
159
+ detailWidth={Math.max(width - 2, 24)}
160
+ focused={focused && controller.detailOpen}
161
+ height={Math.max(height - 1, 1)}
162
+ historyRange={controller.historyRange}
163
+ onDetailTabChange={controller.actions.setDetailTab}
164
+ onHistoryRangeChange={controller.actions.setHistoryRange}
165
+ onPreviewOrder={controller.actions.previewOrder}
166
+ onSelectMarket={controller.actions.selectMarket}
167
+ scrollRef={controller.detailScrollRef}
168
+ selectedRow={controller.selectedRow}
169
+ selectedSummary={controller.selectedSummary}
170
+ />
171
+ </box>
172
+ ) : (
173
+ <box flexGrow={1} backgroundColor={colors.panel} />
174
+ );
175
+
176
+ return (
177
+ <PageStackView
178
+ focused={focused}
179
+ detailOpen={controller.detailOpen && !!controller.selectedSummary}
180
+ onBack={controller.actions.closeDetail}
181
+ rootContent={browseContent}
182
+ detailContent={detailContent}
183
+ />
184
+ );
264
185
  }
@@ -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) =>
@@ -5,7 +5,6 @@ export type PredictionVenue = "polymarket" | "kalshi";
5
5
  export type PredictionVenueScope = "all" | PredictionVenue;
6
6
  export type PredictionBrowseTab = "top" | "ending" | "new" | "watchlist";
7
7
  export type PredictionDetailTab = "overview" | "book" | "trades" | "rules";
8
- export type PredictionFocusRegion = "list" | "detail";
9
8
  export type PredictionHistoryRange = "1D" | "1W" | "1M" | "ALL";
10
9
  export type PredictionVolumeUnit = "usd" | "contracts";
11
10
  export type PredictionSortDirection = "asc" | "desc";
@@ -26,6 +26,11 @@ afterEach(() => {
26
26
  });
27
27
 
28
28
  describe("GloomberbCloudProvider", () => {
29
+ test("reports its manual chart resolution capabilities", () => {
30
+ const provider = new GloomberbCloudProvider();
31
+ expect(provider.getChartResolutionCapabilities()).toEqual(["1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"]);
32
+ });
33
+
29
34
  test("fetches detailed intraday chart history with Twelve Data intervals", async () => {
30
35
  apiClient.ensureVerifiedSession = async () => verifiedUser;
31
36
 
@@ -91,6 +96,30 @@ describe("GloomberbCloudProvider", () => {
91
96
  });
92
97
  });
93
98
 
99
+ test("fetches fixed-resolution chart history with the requested interval", async () => {
100
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
101
+
102
+ let requestArgs: Record<string, string | number | undefined> | null = null;
103
+ apiClient.getCloudHistory = async (_symbol, _exchange, params = {}) => {
104
+ requestArgs = params;
105
+ return {
106
+ status: "success",
107
+ data: [{
108
+ date: "2026-03-27",
109
+ close: 250.12,
110
+ }],
111
+ };
112
+ };
113
+
114
+ const provider = new GloomberbCloudProvider();
115
+ const history = await provider.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1wk");
116
+
117
+ expect(requestArgs?.interval).toBe("1week");
118
+ expect(requestArgs?.startDate).toBeDefined();
119
+ expect(requestArgs?.endDate).toBeDefined();
120
+ expect(history[0]?.close).toBe(250.12);
121
+ });
122
+
94
123
  test("normalizes sub-unit cloud quotes to their main currency", async () => {
95
124
  apiClient.ensureVerifiedSession = async () => verifiedUser;
96
125
  apiClient.getCloudQuote = async () => ({