gloomberb 0.3.3 → 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 (143) hide show
  1. package/README.md +27 -16
  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-renderer.ts +7 -2
  17. package/src/components/chart/chart-resolution.test.ts +56 -0
  18. package/src/components/chart/chart-resolution.ts +362 -0
  19. package/src/components/chart/chart-types.ts +12 -4
  20. package/src/components/chart/chart-viewport.test.ts +25 -0
  21. package/src/components/chart/chart-viewport.ts +28 -0
  22. package/src/components/chart/chart.test.ts +26 -0
  23. package/src/components/chart/comparison-chart-data.ts +20 -27
  24. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  25. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  26. package/src/components/chart/stock-chart.tsx +484 -112
  27. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  28. package/src/components/command-bar/command-bar.test.tsx +650 -5
  29. package/src/components/command-bar/command-bar.tsx +359 -63
  30. package/src/components/command-bar/command-registry.ts +8 -0
  31. package/src/components/command-bar/helpers.test.ts +23 -1
  32. package/src/components/command-bar/helpers.ts +14 -5
  33. package/src/components/command-bar/pane-template-display.ts +12 -0
  34. package/src/components/command-bar/root-shortcuts.ts +2 -1
  35. package/src/components/command-bar/workflow-ops.ts +4 -2
  36. package/src/components/detail-feed-view.test.tsx +124 -49
  37. package/src/components/detail-feed-view.tsx +396 -160
  38. package/src/components/layout/floating-pane.tsx +1 -1
  39. package/src/components/layout/pane-header.tsx +26 -2
  40. package/src/components/layout/shell.tsx +62 -50
  41. package/src/components/layout/status-bar.test.tsx +66 -0
  42. package/src/components/layout/status-bar.tsx +26 -5
  43. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  44. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  45. package/src/components/ticker-list-table.test.tsx +83 -0
  46. package/src/components/ticker-list-table.tsx +174 -97
  47. package/src/components/ui/data-table.tsx +27 -2
  48. package/src/components/ui/index.ts +1 -0
  49. package/src/components/ui/page-stack-view.tsx +60 -0
  50. package/src/components/use-double-click-activation.ts +31 -0
  51. package/src/index.tsx +10 -10
  52. package/src/market-data/coordinator.test.ts +92 -4
  53. package/src/market-data/coordinator.ts +32 -5
  54. package/src/market-data/hooks.test.tsx +120 -0
  55. package/src/market-data/hooks.tsx +53 -16
  56. package/src/market-data/request-types.ts +4 -2
  57. package/src/market-data/selectors.ts +24 -58
  58. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  59. package/src/plugins/builtin/chat-controller.ts +310 -35
  60. package/src/plugins/builtin/chat.test.tsx +118 -6
  61. package/src/plugins/builtin/chat.tsx +175 -116
  62. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  63. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  64. package/src/plugins/builtin/news.tsx +1 -11
  65. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  66. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  67. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  68. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  69. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  70. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  71. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  72. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  73. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  74. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  75. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  76. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  77. package/src/plugins/builtin/sec.tsx +1 -11
  78. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  79. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  80. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  81. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  82. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  83. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  84. package/src/plugins/catalog.ts +65 -0
  85. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  86. package/src/plugins/ibkr/gateway-service.test.ts +75 -1
  87. package/src/plugins/ibkr/gateway-service.ts +179 -9
  88. package/src/plugins/pane-manager.test.ts +1 -1
  89. package/src/plugins/plugin-runtime.tsx +27 -7
  90. package/src/plugins/prediction-markets/chart.test.tsx +139 -0
  91. package/src/plugins/prediction-markets/chart.tsx +65 -436
  92. package/src/plugins/prediction-markets/cli.ts +28 -0
  93. package/src/plugins/prediction-markets/controller-data.ts +17 -5
  94. package/src/plugins/prediction-markets/controller.ts +141 -121
  95. package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
  96. package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
  97. package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
  98. package/src/plugins/prediction-markets/detail.test.tsx +4 -0
  99. package/src/plugins/prediction-markets/index.tsx +2 -0
  100. package/src/plugins/prediction-markets/launch.ts +51 -16
  101. package/src/plugins/prediction-markets/metrics.ts +9 -5
  102. package/src/plugins/prediction-markets/pane.test.tsx +160 -58
  103. package/src/plugins/prediction-markets/pane.tsx +74 -183
  104. package/src/plugins/prediction-markets/table.tsx +6 -3
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/sources/gloomberb-cloud.test.ts +29 -0
  107. package/src/sources/gloomberb-cloud.ts +89 -1
  108. package/src/sources/provider-router.test.ts +308 -3
  109. package/src/sources/provider-router.ts +247 -17
  110. package/src/sources/yahoo-finance.test.ts +26 -0
  111. package/src/sources/yahoo-finance.ts +54 -3
  112. package/src/state/app-context-selector.test.tsx +99 -0
  113. package/src/state/app-context.test.ts +18 -0
  114. package/src/state/app-context.tsx +170 -65
  115. package/src/state/selectors.ts +25 -0
  116. package/src/theme/colors.ts +8 -8
  117. package/src/theme/themes.test.ts +2 -0
  118. package/src/theme/themes.ts +22 -10
  119. package/src/types/broker.ts +21 -0
  120. package/src/types/config.ts +17 -1
  121. package/src/types/data-provider.ts +18 -0
  122. package/src/types/financials.ts +31 -1
  123. package/src/types/plugin.ts +70 -0
  124. package/src/utils/api-client.test.ts +57 -0
  125. package/src/utils/api-client.ts +35 -12
  126. package/src/utils/back-navigation.ts +27 -0
  127. package/src/utils/format.test.ts +14 -0
  128. package/src/utils/format.ts +3 -3
  129. package/src/utils/market-status.ts +46 -1
  130. package/src/utils/price-history.test.ts +25 -0
  131. package/src/utils/price-history.ts +29 -5
  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
@@ -1,27 +1,6 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
2
- import { TextAttributes, type BoxRenderable } from "@opentui/core";
3
- import { useRenderer } from "@opentui/react";
4
- import {
5
- buildChartScene,
6
- renderChart,
7
- resolveChartPalette,
8
- type StyledContent,
9
- } from "../../components/chart/chart-renderer";
10
- import {
11
- computeBitmapSize,
12
- renderNativeChartBase,
13
- type CellRect,
14
- type NativeChartBitmap,
15
- } from "../../components/chart/native/chart-rasterizer";
16
- import {
17
- ensureKittySupport,
18
- getCachedKittySupport,
19
- } from "../../components/chart/native/kitty-support";
20
- import { resolveChartRendererState } from "../../components/chart/native/renderer-selection";
21
- import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
22
- import { syncCachedNativeSurface } from "../../components/chart/native/surface-sync";
23
- import { projectChartData } from "../../components/chart/chart-data";
24
- import { useAppState, usePaneInstanceId } from "../../state/app-context";
1
+ import { useMemo } from "react";
2
+ import { TextAttributes } from "@opentui/core";
3
+ import { StockChart } from "../../components/chart/stock-chart";
25
4
  import { EmptyState } from "../../components/ui/status";
26
5
  import { colors } from "../../theme/colors";
27
6
  import { formatNumber, formatPercentRaw } from "../../utils/format";
@@ -29,15 +8,6 @@ import type { PricePoint } from "../../types/financials";
29
8
  import type { PredictionHistoryPoint, PredictionHistoryRange } from "./types";
30
9
 
31
10
  const RANGES: PredictionHistoryRange[] = ["1D", "1W", "1M", "ALL"];
32
- const AXIS_WIDTH = 8;
33
-
34
- interface RenderableNode {
35
- x: number;
36
- y: number;
37
- width: number;
38
- height: number;
39
- parent: RenderableNode | null;
40
- }
41
11
 
42
12
  function coercePredictionPointDate(value: unknown): Date | null {
43
13
  if (value instanceof Date) {
@@ -54,79 +24,47 @@ function toPricePoints(points: PredictionHistoryPoint[]): PricePoint[] {
54
24
  return points.flatMap((point) => {
55
25
  const date = coercePredictionPointDate(point.date);
56
26
  if (!date) return [];
57
- return [
58
- {
59
- date,
60
- close: point.close,
61
- open: point.open,
62
- high: point.high,
63
- low: point.low,
64
- volume: point.volume,
65
- },
66
- ];
27
+ return [{
28
+ date,
29
+ close: point.close,
30
+ open: point.open,
31
+ high: point.high,
32
+ low: point.low,
33
+ volume: point.volume,
34
+ }];
67
35
  });
68
36
  }
69
37
 
70
- function extractCellRect(renderable: RenderableNode): CellRect {
71
- return {
72
- x: renderable.x,
73
- y: renderable.y,
74
- width: renderable.width,
75
- height: renderable.height,
76
- };
77
- }
78
-
79
- function intersectCellRects(left: CellRect, right: CellRect): CellRect | null {
80
- const x = Math.max(left.x, right.x);
81
- const y = Math.max(left.y, right.y);
82
- const maxX = Math.min(left.x + left.width, right.x + right.width);
83
- const maxY = Math.min(left.y + left.height, right.y + right.height);
84
- if (maxX <= x || maxY <= y) return null;
85
- return { x, y, width: maxX - x, height: maxY - y };
86
- }
87
-
88
- function resolveVisibleRect(
89
- renderable: RenderableNode | null,
90
- terminalWidth: number,
91
- terminalHeight: number,
92
- ): CellRect | null {
93
- if (!renderable) return null;
94
-
95
- let visible: CellRect = {
96
- x: 0,
97
- y: 0,
98
- width: terminalWidth,
99
- height: terminalHeight,
100
- };
101
- let current: RenderableNode | null = renderable;
102
-
103
- while (current) {
104
- const nextVisible = intersectCellRects(visible, extractCellRect(current));
105
- if (!nextVisible) return null;
106
- visible = nextVisible;
107
- current = current.parent;
108
- }
109
-
110
- return visible;
111
- }
112
-
113
- function buildBlankPlotLines(width: number, height: number): string[] {
114
- return Array.from({ length: height }, () => " ".repeat(width));
115
- }
116
-
117
- function buildNativeBitmapKey(
118
- points: PricePoint[],
119
- pixelWidth: number,
120
- pixelHeight: number,
121
- paletteId: string,
122
- ): string {
123
- const fingerprint = points
124
- .map(
125
- (point) =>
126
- `${point.date.getTime()}:${point.open}:${point.high}:${point.low}:${point.close}:${point.volume ?? 0}`,
127
- )
128
- .join("|");
129
- return [pixelWidth, pixelHeight, paletteId, fingerprint].join("::");
38
+ function PredictionRangeTabs({
39
+ activeRange,
40
+ onRangeSelect,
41
+ }: {
42
+ activeRange: PredictionHistoryRange;
43
+ onRangeSelect: (range: PredictionHistoryRange) => void;
44
+ }) {
45
+ return (
46
+ <box flexDirection="row" gap={1}>
47
+ {RANGES.map((entry) => {
48
+ const active = entry === activeRange;
49
+ return (
50
+ <box
51
+ key={entry}
52
+ onMouseDown={(event) => {
53
+ event.preventDefault();
54
+ onRangeSelect(entry);
55
+ }}
56
+ >
57
+ <text
58
+ fg={active ? colors.textBright : colors.textDim}
59
+ attributes={active ? TextAttributes.BOLD : 0}
60
+ >
61
+ {entry}
62
+ </text>
63
+ </box>
64
+ );
65
+ })}
66
+ </box>
67
+ );
130
68
  }
131
69
 
132
70
  export function PredictionMarketChart({
@@ -142,299 +80,16 @@ export function PredictionMarketChart({
142
80
  range: PredictionHistoryRange;
143
81
  onRangeSelect: (range: PredictionHistoryRange) => void;
144
82
  }) {
145
- const { state } = useAppState();
146
- const renderer = useRenderer();
147
- const paneId = usePaneInstanceId();
148
- const nativeSurfaceManager = useMemo(
149
- () => getNativeSurfaceManager(renderer),
150
- [renderer],
151
- );
152
- const [kittySupport, setKittySupport] = useState<boolean | null>(() =>
153
- getCachedKittySupport(renderer),
154
- );
155
- const plotRef = useRef<BoxRenderable>(null);
156
- const nativeSurfaceIdRef = useRef(`prediction-chart-surface:${paneId}`);
157
- const lastNativeBitmapRef = useRef<{
158
- key: string;
159
- bitmap: NativeChartBitmap;
160
- } | null>(null);
161
- const lastNativeGeometryRef = useRef<{
162
- rect: CellRect;
163
- visibleRect: CellRect | null;
164
- } | null>(null);
165
-
166
- const chartWidth = Math.max(width - AXIS_WIDTH - 2, 18);
167
- const chartHeight = Math.max(height - 2, 6);
168
- const hasHistory = history.length > 0;
169
-
170
- useEffect(() => {
171
- if (kittySupport !== null) return;
172
- let disposed = false;
173
- ensureKittySupport(renderer)
174
- .then((supported) => {
175
- if (!disposed) setKittySupport(supported);
176
- })
177
- .catch(() => {
178
- if (!disposed) setKittySupport(false);
179
- });
180
- return () => {
181
- disposed = true;
182
- };
183
- }, [kittySupport, renderer]);
184
-
185
83
  const pricePoints = useMemo(() => toPricePoints(history), [history]);
186
- const projection = useMemo(
187
- () => projectChartData(pricePoints, chartWidth, "area"),
188
- [chartWidth, pricePoints],
189
- );
190
- const first = projection.points[0] ?? null;
191
- const last = projection.points[projection.points.length - 1] ?? null;
192
- const delta = first && last ? last.close - first.close : 0;
193
- const deltaPct = first?.close ? (delta / first.close) * 100 : 0;
194
- const palette = useMemo(
195
- () =>
196
- resolveChartPalette(
197
- colors,
198
- delta > 0 ? "positive" : delta < 0 ? "negative" : "neutral",
199
- ),
200
- [delta],
201
- );
202
- const rendererState = resolveChartRendererState(
203
- state.config.chartPreferences.renderer,
204
- kittySupport,
205
- renderer.resolution,
206
- );
207
- const effectiveRenderer = rendererState.renderer;
208
84
 
209
- const rendered = useMemo(
210
- () =>
211
- renderChart(projection.points, {
212
- width: chartWidth,
213
- height: chartHeight,
214
- mode: "area",
215
- showVolume: false,
216
- volumeHeight: 0,
217
- cursorX: null,
218
- cursorY: null,
219
- colors: palette,
220
- axisMode: "price",
221
- currency: "USD",
222
- }),
223
- [chartHeight, chartWidth, palette, projection.points],
224
- );
225
-
226
- const nativeScene = useMemo(
227
- () =>
228
- buildChartScene(projection.points, {
229
- width: chartWidth,
230
- height: chartHeight,
231
- showVolume: false,
232
- volumeHeight: 0,
233
- cursorX: null,
234
- cursorY: null,
235
- mode: "area",
236
- axisMode: "price",
237
- colors: palette,
238
- }),
239
- [chartHeight, chartWidth, palette, projection.points],
240
- );
241
-
242
- const axisByRow = useMemo(
243
- () => new Map(rendered.axisLabels.map((entry) => [entry.row, entry.label])),
244
- [rendered.axisLabels],
245
- );
246
- const blankPlotLines = useMemo(
247
- () => buildBlankPlotLines(chartWidth, chartHeight),
248
- [chartHeight, chartWidth],
249
- );
250
-
251
- useEffect(
252
- () => () => {
253
- lastNativeBitmapRef.current = null;
254
- lastNativeGeometryRef.current = null;
255
- nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
256
- renderer.requestRender();
257
- },
258
- [nativeSurfaceManager, renderer],
259
- );
260
-
261
- useEffect(() => {
262
- if (effectiveRenderer === "kitty" && rendererState.nativeReady) return;
263
- lastNativeBitmapRef.current = null;
264
- lastNativeGeometryRef.current = null;
265
- nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
266
- }, [effectiveRenderer, nativeSurfaceManager, rendererState.nativeReady]);
267
-
268
- useEffect(() => {
269
- if (
270
- effectiveRenderer !== "kitty" ||
271
- !rendererState.nativeReady ||
272
- !plotRef.current
273
- )
274
- return;
275
- const plot = plotRef.current;
276
- let mountTimer: ReturnType<typeof setTimeout> | null = null;
277
-
278
- const syncPlacement = () => {
279
- if (
280
- effectiveRenderer !== "kitty" ||
281
- !rendererState.nativeReady ||
282
- !plotRef.current
283
- )
284
- return;
285
- const rect = extractCellRect(plotRef.current);
286
- const visibleRect = resolveVisibleRect(
287
- plotRef.current,
288
- renderer.terminalWidth,
289
- renderer.terminalHeight,
290
- );
291
- const previous = lastNativeGeometryRef.current;
292
- if (
293
- previous &&
294
- previous.rect.x === rect.x &&
295
- previous.rect.y === rect.y &&
296
- previous.rect.width === rect.width &&
297
- previous.rect.height === rect.height &&
298
- previous.visibleRect?.x === visibleRect?.x &&
299
- previous.visibleRect?.y === visibleRect?.y &&
300
- previous.visibleRect?.width === visibleRect?.width &&
301
- previous.visibleRect?.height === visibleRect?.height
302
- ) {
303
- return;
304
- }
305
- lastNativeGeometryRef.current = { rect, visibleRect };
306
- syncCachedNativeSurface(
307
- nativeSurfaceManager,
308
- nativeSurfaceIdRef.current,
309
- { paneId, rect, visibleRect },
310
- lastNativeBitmapRef.current,
311
- );
312
- };
313
-
314
- plot.onLifecyclePass = syncPlacement;
315
- renderer.registerLifecyclePass(plot);
316
- syncPlacement();
317
- mountTimer = setTimeout(() => {
318
- syncPlacement();
319
- renderer.requestRender();
320
- }, 0);
321
-
322
- return () => {
323
- if (mountTimer) clearTimeout(mountTimer);
324
- plot.onLifecyclePass = null;
325
- renderer.unregisterLifecyclePass(plot);
326
- lastNativeGeometryRef.current = null;
327
- };
328
- }, [
329
- effectiveRenderer,
330
- nativeSurfaceManager,
331
- paneId,
332
- renderer,
333
- rendererState.nativeReady,
334
- ]);
335
-
336
- useEffect(() => {
337
- if (
338
- effectiveRenderer !== "kitty" ||
339
- !rendererState.nativeReady ||
340
- !renderer.resolution ||
341
- !plotRef.current ||
342
- !nativeScene
343
- ) {
344
- lastNativeBitmapRef.current = null;
345
- nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
346
- return;
347
- }
348
-
349
- const plotRect = extractCellRect(plotRef.current);
350
- const visibleRect = resolveVisibleRect(
351
- plotRef.current,
352
- renderer.terminalWidth,
353
- renderer.terminalHeight,
354
- );
355
- if (!visibleRect) {
356
- nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
357
- return;
358
- }
359
-
360
- const bitmapSize = computeBitmapSize(
361
- plotRect,
362
- renderer.resolution,
363
- renderer.terminalWidth,
364
- renderer.terminalHeight,
365
- );
366
- const bitmapKey = buildNativeBitmapKey(
367
- pricePoints,
368
- bitmapSize.pixelWidth,
369
- bitmapSize.pixelHeight,
370
- [palette.lineColor, palette.fillColor, palette.gridColor].join(","),
371
- );
372
- const cachedBitmap =
373
- lastNativeBitmapRef.current?.key === bitmapKey
374
- ? lastNativeBitmapRef.current.bitmap
375
- : null;
376
- const bitmap =
377
- cachedBitmap ??
378
- renderNativeChartBase(
379
- nativeScene,
380
- bitmapSize.pixelWidth,
381
- bitmapSize.pixelHeight,
382
- );
383
- if (!cachedBitmap) {
384
- lastNativeBitmapRef.current = { key: bitmapKey, bitmap };
385
- }
386
-
387
- nativeSurfaceManager.upsertSurface({
388
- id: nativeSurfaceIdRef.current,
389
- paneId,
390
- rect: plotRect,
391
- visibleRect,
392
- bitmap,
393
- bitmapKey,
394
- });
395
- renderer.requestRender();
396
- }, [
397
- effectiveRenderer,
398
- nativeScene,
399
- nativeSurfaceManager,
400
- paneId,
401
- palette.fillColor,
402
- palette.gridColor,
403
- palette.lineColor,
404
- pricePoints,
405
- renderer,
406
- rendererState.nativeReady,
407
- ]);
408
-
409
- const plotLines: Array<string | StyledContent> =
410
- effectiveRenderer === "kitty" ? blankPlotLines : rendered.lines;
411
- const plotContent = plotLines.map((line, index) => (
412
- <text key={index} content={line as any} />
413
- ));
414
-
415
- if (!hasHistory) {
85
+ if (pricePoints.length === 0) {
416
86
  return (
417
87
  <box flexDirection="column" height={height}>
418
- <box flexDirection="row" gap={1} height={1}>
419
- {RANGES.map((entry) => {
420
- const active = entry === range;
421
- return (
422
- <box
423
- key={entry}
424
- onMouseDown={(event) => {
425
- event.preventDefault();
426
- onRangeSelect(entry);
427
- }}
428
- >
429
- <text
430
- fg={active ? colors.textBright : colors.textDim}
431
- attributes={active ? TextAttributes.BOLD : 0}
432
- >
433
- {entry}
434
- </text>
435
- </box>
436
- );
437
- })}
88
+ <box flexDirection="row" height={1}>
89
+ <PredictionRangeTabs
90
+ activeRange={range}
91
+ onRangeSelect={onRangeSelect}
92
+ />
438
93
  </box>
439
94
  <box flexGrow={1} justifyContent="center">
440
95
  <EmptyState
@@ -446,30 +101,19 @@ export function PredictionMarketChart({
446
101
  );
447
102
  }
448
103
 
104
+ const first = pricePoints[0] ?? null;
105
+ const last = pricePoints[pricePoints.length - 1] ?? null;
106
+ const delta = first && last ? last.close - first.close : 0;
107
+ const deltaPct = first?.close ? (delta / first.close) * 100 : 0;
108
+ const chartHeight = Math.max(height - 1, 2);
109
+
449
110
  return (
450
111
  <box flexDirection="column" height={height}>
451
112
  <box flexDirection="row" height={1}>
452
- <box flexDirection="row" gap={1}>
453
- {RANGES.map((entry) => {
454
- const active = entry === range;
455
- return (
456
- <box
457
- key={entry}
458
- onMouseDown={(event) => {
459
- event.preventDefault();
460
- onRangeSelect(entry);
461
- }}
462
- >
463
- <text
464
- fg={active ? colors.textBright : colors.textDim}
465
- attributes={active ? TextAttributes.BOLD : 0}
466
- >
467
- {entry}
468
- </text>
469
- </box>
470
- );
471
- })}
472
- </box>
113
+ <PredictionRangeTabs
114
+ activeRange={range}
115
+ onRangeSelect={onRangeSelect}
116
+ />
473
117
  <box flexGrow={1} />
474
118
  <text
475
119
  fg={
@@ -484,29 +128,14 @@ export function PredictionMarketChart({
484
128
  </text>
485
129
  </box>
486
130
 
487
- <box flexDirection="row" height={chartHeight}>
488
- <box
489
- ref={plotRef}
490
- width={chartWidth}
491
- height={chartHeight}
492
- flexDirection="column"
493
- backgroundColor={palette.bgColor}
494
- >
495
- {plotContent}
496
- </box>
497
- <box width={1} />
498
- <box width={AXIS_WIDTH} flexDirection="column" height={chartHeight}>
499
- {Array.from({ length: chartHeight }, (_, index) => (
500
- <text key={index} fg={colors.textDim}>
501
- {axisByRow.get(index) ?? ""}
502
- </text>
503
- ))}
504
- </box>
505
- </box>
506
-
507
- <box height={1}>
508
- <text fg={colors.textDim}>{rendered.timeLabels}</text>
509
- </box>
131
+ <StockChart
132
+ width={width}
133
+ height={chartHeight}
134
+ focused={false}
135
+ compact
136
+ historyOverride={pricePoints}
137
+ currencyOverride="USD"
138
+ />
510
139
  </box>
511
140
  );
512
141
  }
@@ -0,0 +1,28 @@
1
+ import type { CliCommandDef } from "../../types/plugin";
2
+ import { createPredictionLaunchRequest, parsePredictionCommandArgs } from "./launch";
3
+
4
+ export const predictionMarketsCliCommand: CliCommandDef = {
5
+ name: "predictions",
6
+ aliases: ["prediction-markets", "pm"],
7
+ description: "Launch the UI with Prediction Markets focused",
8
+ help: {
9
+ usage: ["predictions [...]"],
10
+ sections: [{
11
+ title: "Prediction Launch",
12
+ columns: [
13
+ { header: "Input" },
14
+ { header: "Example" },
15
+ ],
16
+ rows: [
17
+ ["gloomberb predictions [venue] [category] [browse-tab] [search...]", "gloomberb predictions world"],
18
+ ["venue", "all | polymarket | kalshi"],
19
+ ["category", "all | politics | world | macro | crypto | science | sports | entertainment | climate | social"],
20
+ ["browse-tab", "top | ending | new | watchlist"],
21
+ ],
22
+ }],
23
+ },
24
+ execute: async (args) => ({
25
+ kind: "launch-ui",
26
+ request: createPredictionLaunchRequest(parsePredictionCommandArgs(args)),
27
+ }),
28
+ };
@@ -39,6 +39,7 @@ const KEYBOARD_DETAIL_LOAD_DELAY_MS = 140;
39
39
  export function usePredictionMarketsDataState({
40
40
  browseTab,
41
41
  categoryId,
42
+ detailOpen,
42
43
  effectiveVenueScope,
43
44
  focused,
44
45
  historyRange,
@@ -52,6 +53,7 @@ export function usePredictionMarketsDataState({
52
53
  }: {
53
54
  browseTab: PredictionBrowseTab;
54
55
  categoryId: PredictionCategoryId;
56
+ detailOpen: boolean;
55
57
  effectiveVenueScope: PredictionVenueScope;
56
58
  focused: boolean;
57
59
  historyRange: "1D" | "1W" | "1M" | "ALL";
@@ -201,7 +203,11 @@ export function usePredictionMarketsDataState({
201
203
  [selectedRowKey, visibleRows],
202
204
  );
203
205
  const selectedSummary = useMemo(
204
- () =>
206
+ () => {
207
+ if (!detailOpen || !selectedRow) {
208
+ return null;
209
+ }
210
+ return (
205
211
  selectedRow?.markets.find(
206
212
  (market) => market.key === selectedDetailMarketKey,
207
213
  ) ??
@@ -209,8 +215,10 @@ export function usePredictionMarketsDataState({
209
215
  (market) => market.key === selectedRow.focusMarketKey,
210
216
  ) ??
211
217
  selectedRow?.representative ??
212
- null,
213
- [selectedDetailMarketKey, selectedRow],
218
+ null
219
+ );
220
+ },
221
+ [detailOpen, selectedDetailMarketKey, selectedRow],
214
222
  );
215
223
  const selectedSummaryKey = selectedSummary?.key ?? null;
216
224
  const selectedSummaryVenue = selectedSummary?.venue ?? null;
@@ -431,14 +439,18 @@ export function usePredictionMarketsDataState({
431
439
  setTransportState("idle");
432
440
  return;
433
441
  }
434
- const detailSummary = selectedSummaryRef.current ?? selectedSummary;
442
+ const detailSummary = selectedSummaryRef.current;
443
+ if (!detailSummary) {
444
+ setTransportState("idle");
445
+ return;
446
+ }
435
447
  const delayMs = detailLoadDelayRef.current;
436
448
  detailLoadDelayRef.current = 0;
437
449
  const timeoutId = setTimeout(() => {
438
450
  void loadSelectedDetail(detailSummary);
439
451
  }, delayMs);
440
452
  return () => clearTimeout(timeoutId);
441
- }, [loadSelectedDetail, selectedSummaryKey, selectedSummary]);
453
+ }, [loadSelectedDetail, selectedSummaryKey]);
442
454
 
443
455
  useEffect(() => {
444
456
  if (!focused || selectedSummaryVenue !== "kalshi" || !selectedSummaryKey) {