gloomberb 0.9.3 → 0.9.5

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 (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -48,4 +48,54 @@ describe("prediction markets detail views", () => {
48
48
  expect(frame).not.toContain("Kalshi");
49
49
  expect(frame).not.toContain("targets");
50
50
  });
51
+
52
+ test("keeps grouped outcomes in place while selected detail is loading", async () => {
53
+ testSetup = await testRender(<GroupedDetailHarness loading />, {
54
+ width: 64,
55
+ height: 24,
56
+ });
57
+ await flushFrames(testSetup);
58
+
59
+ const frame = testSetup.captureCharFrame();
60
+ expect(frame).toContain("Outcomes");
61
+ expect(frame).toContain("Above 4.25%");
62
+ expect(frame).toContain("Loading chart...");
63
+ expect(frame).not.toContain("No chart history.");
64
+ expect(frame).not.toContain("Loading market detail...");
65
+ });
66
+
67
+ test("renders book levels in the shared data table", async () => {
68
+ testSetup = await testRender(<GroupedDetailHarness detailTab="book" />, {
69
+ width: 64,
70
+ height: 24,
71
+ });
72
+ await flushFrames(testSetup);
73
+
74
+ const frame = testSetup.captureCharFrame();
75
+ expect(frame).toContain("OUT");
76
+ expect(frame).toContain("SIDE");
77
+ expect(frame).toContain("PRICE");
78
+ expect(frame).toContain("SIZE");
79
+ expect(frame).toContain("YES");
80
+ expect(frame).toContain("BID");
81
+ expect(frame).toContain("47c");
82
+ });
83
+
84
+ test("renders recent trades in the shared data table", async () => {
85
+ testSetup = await testRender(<GroupedDetailHarness detailTab="trades" />, {
86
+ width: 64,
87
+ height: 24,
88
+ });
89
+ await flushFrames(testSetup);
90
+
91
+ const frame = testSetup.captureCharFrame();
92
+ expect(frame).toContain("TIME");
93
+ expect(frame).toContain("SIDE");
94
+ expect(frame).toContain("OUT");
95
+ expect(frame).toContain("PRICE");
96
+ expect(frame).toContain("SIZE");
97
+ expect(frame).toContain("BUY");
98
+ expect(frame).toContain("YES");
99
+ expect(frame).toContain("48c");
100
+ });
51
101
  });
@@ -130,6 +130,32 @@ describe("prediction markets pane interactions", () => {
130
130
  expect(frame).not.toContain("Was there a typo in the url or port?");
131
131
  });
132
132
 
133
+ test("shows loading instead of an empty state while empty catalogs are pending", async () => {
134
+ attachPredictionMarketsPersistence(new MemoryPersistence());
135
+
136
+ const releaseFetches: Array<() => void> = [];
137
+ globalThis.fetch = (async (input: Request | string | URL) => {
138
+ await new Promise<void>((resolve) => {
139
+ releaseFetches.push(resolve);
140
+ });
141
+ const url = String(input);
142
+ if (url.includes("/trade-api/v2/events?")) {
143
+ return new Response(JSON.stringify({ events: [] }), { status: 200 });
144
+ }
145
+ return new Response(JSON.stringify([]), { status: 200 });
146
+ }) as unknown as typeof fetch;
147
+
148
+ testSetup = await testRender(<Harness />, { width: 120, height: 34 });
149
+ await flushFrames(testSetup);
150
+
151
+ const frame = testSetup.captureCharFrame();
152
+ expect(frame).toContain("Loading markets...");
153
+ expect(frame).not.toContain("No markets matched.");
154
+
155
+ for (const release of releaseFetches) release();
156
+ await flushFrames(testSetup);
157
+ });
158
+
133
159
  test("selects a market on single click and opens detail on double click", async () => {
134
160
  installPredictionMarketMocks();
135
161
 
@@ -172,6 +198,23 @@ describe("prediction markets pane interactions", () => {
172
198
 
173
199
  frame = testSetup.captureCharFrame();
174
200
  expect(frame).toContain("Kalshi primary rule");
201
+ expect(frame).toContain("\u2190 Back Will the Fed cut rates?");
202
+ expect(frame.match(/Will the Fed cut rates\?/g) ?? []).toHaveLength(1);
203
+ expect(frame).not.toContain("[/]search");
204
+ expect(frame).not.toContain("[w]atch");
205
+ expect(frame).not.toContain("[1-4]browse");
206
+
207
+ const metricsHeader = frame
208
+ .split("\n")
209
+ .find((line) =>
210
+ line.includes("YES") &&
211
+ line.includes("NO") &&
212
+ line.includes("24H VOL"),
213
+ );
214
+ expect(metricsHeader).toContain("TOTAL VOL");
215
+ expect(metricsHeader).toContain("OI");
216
+ expect(metricsHeader).toContain("SPREAD");
217
+ expect(metricsHeader).toContain("LAST");
175
218
  });
176
219
 
177
220
  test("focuses the pane when a market row is clicked", async () => {
@@ -2,6 +2,7 @@ import { Box, Input, Text } from "../../ui";
2
2
  import { useCallback, useMemo, useRef } from "react";
3
3
  import {
4
4
  DataTableStackView,
5
+ Spinner,
5
6
  Tabs,
6
7
  usePaneFooter,
7
8
  type DataTableKeyEvent,
@@ -13,6 +14,7 @@ import { colors } from "../../theme/colors";
13
14
  import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
14
15
  import { usePredictionMarketsController } from "./controller";
15
16
  import { PredictionMarketDetailPane } from "./detail/pane";
17
+ import { resolvePredictionDetailTitle } from "./detail/shared";
16
18
  import { getPredictionColumnValue } from "./metrics";
17
19
  import { BROWSE_TABS, VENUE_TABS } from "./navigation";
18
20
  import { isPlainArrowUp, stopSearchFocusNavigation } from "../../utils/search-focus-navigation";
@@ -73,33 +75,45 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
73
75
  controller.catalogStatus?.tone === "danger"
74
76
  ? colors.negative
75
77
  : colors.borderFocused;
76
- usePaneFooter("prediction-markets", () => ({
77
- info: [
78
- ...(controller.searchQuery.trim() ? [{ id: "search", parts: [{ text: `search: ${controller.searchQuery.trim()}`, tone: "value" as const }] }] : []),
79
- ...(controller.searchLoading ? [{ id: "search-loading", parts: [{ text: "searching", tone: "muted" as const }] }] : []),
80
- ...(controller.catalogStatus ? [{
81
- id: "catalog",
82
- parts: [{ text: controller.catalogStatus.message, tone: controller.catalogStatus.tone === "danger" ? "warning" as const : "muted" as const, color: catalogStatusColor }],
83
- }] : []),
84
- ],
85
- hints: [
86
- { id: "search", key: "/", label: "search", onPress: controller.actions.focusSearch },
87
- { id: "watch", key: "w", label: "atch", onPress: controller.selectedRow ? () => controller.actions.toggleWatchlist(controller.selectedRow!) : undefined, disabled: !controller.selectedRow },
88
- {
89
- id: "browse",
90
- key: "1-4",
91
- label: "browse",
92
- onPress: () => {
93
- const index = BROWSE_TABS.findIndex((tab) => tab.value === controller.browseTab);
94
- controller.actions.selectBrowseTab(BROWSE_TABS[(index + 1) % BROWSE_TABS.length]!.value as PredictionBrowseTab);
78
+ const rowsLoading =
79
+ controller.visibleRows.length === 0 &&
80
+ (controller.catalogLoadCount > 0 || controller.searchLoading);
81
+ const detailTitle = resolvePredictionDetailTitle({
82
+ detail: controller.detail,
83
+ selectedRow: controller.selectedRow,
84
+ selectedSummary: controller.selectedSummary,
85
+ });
86
+ usePaneFooter("prediction-markets", () => {
87
+ if (controller.detailOpen) return null;
88
+ return {
89
+ info: [
90
+ ...(controller.searchQuery.trim() ? [{ id: "search", parts: [{ text: `search: ${controller.searchQuery.trim()}`, tone: "value" as const }] }] : []),
91
+ ...(controller.searchLoading ? [{ id: "search-loading", parts: [{ text: "searching", tone: "muted" as const }] }] : []),
92
+ ...(controller.catalogStatus ? [{
93
+ id: "catalog",
94
+ parts: [{ text: controller.catalogStatus.message, tone: controller.catalogStatus.tone === "danger" ? "warning" as const : "muted" as const, color: catalogStatusColor }],
95
+ }] : []),
96
+ ],
97
+ hints: [
98
+ { id: "search", key: "/", label: "search", onPress: controller.actions.focusSearch },
99
+ { id: "watch", key: "w", label: "atch", onPress: controller.selectedRow ? () => controller.actions.toggleWatchlist(controller.selectedRow!) : undefined, disabled: !controller.selectedRow },
100
+ {
101
+ id: "browse",
102
+ key: "1-4",
103
+ label: "browse",
104
+ onPress: () => {
105
+ const index = BROWSE_TABS.findIndex((tab) => tab.value === controller.browseTab);
106
+ controller.actions.selectBrowseTab(BROWSE_TABS[(index + 1) % BROWSE_TABS.length]!.value as PredictionBrowseTab);
107
+ },
95
108
  },
96
- },
97
- ],
98
- }), [
109
+ ],
110
+ };
111
+ }, [
99
112
  catalogStatusColor,
100
113
  controller.browseTab,
101
114
  controller.catalogStatus?.message,
102
115
  controller.catalogStatus?.tone,
116
+ controller.detailOpen,
103
117
  controller.searchLoading,
104
118
  controller.searchQuery,
105
119
  controller.selectedRow,
@@ -275,6 +289,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
275
289
  detailOpen={controller.detailOpen && !!controller.selectedSummary}
276
290
  onBack={controller.actions.closeDetail}
277
291
  detailContent={detailContent}
292
+ detailTitle={detailTitle}
278
293
  rootBefore={browseControls}
279
294
  rootWidth={width}
280
295
  rootHeight={height}
@@ -301,6 +316,19 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
301
316
  getItemKey={(row) => row.key}
302
317
  virtualize
303
318
  renderCell={renderCell}
319
+ emptyContent={
320
+ rowsLoading ? (
321
+ <Box width="100%" paddingX={1} paddingY={1}>
322
+ <Spinner
323
+ label={
324
+ controller.searchQuery.trim().length > 0
325
+ ? "Searching markets..."
326
+ : "Loading markets..."
327
+ }
328
+ />
329
+ </Box>
330
+ ) : undefined
331
+ }
304
332
  emptyStateTitle="No markets matched."
305
333
  emptyStateHint="Change the venue, browse tab, or search query."
306
334
  />
@@ -24,6 +24,7 @@ import {
24
24
  attachPredictionMarketsPersistence,
25
25
  } from "./services/fetch";
26
26
  import { normalizeKalshiMarket } from "./services/kalshi/adapter";
27
+ import type { PredictionDetailTab } from "./types";
27
28
 
28
29
  export const TEST_PANE_ID = "prediction-markets:main";
29
30
  const originalFetch = globalThis.fetch;
@@ -408,7 +409,13 @@ export function ChartHarness({
408
409
  );
409
410
  }
410
411
 
411
- export function GroupedDetailHarness() {
412
+ export function GroupedDetailHarness({
413
+ detailTab = "overview",
414
+ loading = false,
415
+ }: {
416
+ detailTab?: PredictionDetailTab;
417
+ loading?: boolean;
418
+ } = {}) {
412
419
  const [state, dispatch] = useReducer(
413
420
  appReducer,
414
421
  (() => {
@@ -475,29 +482,40 @@ export function GroupedDetailHarness() {
475
482
  <AppContext value={{ state, dispatch }}>
476
483
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
477
484
  <PredictionMarketDetailPane
478
- detail={{
479
- summary,
480
- siblings: selectedRow!.markets.map((market) => ({
481
- key: market.key,
482
- marketId: market.marketId,
483
- label: market.marketLabel,
484
- yesPrice: market.yesPrice,
485
- volume24h: market.volume24h,
486
- })),
487
- rules: [],
488
- history: [],
489
- book: {
490
- yesBids: [],
491
- yesAsks: [],
492
- noBids: [],
493
- noAsks: [],
494
- lastTradePrice: null,
495
- },
496
- trades: [],
497
- }}
485
+ detail={
486
+ loading
487
+ ? null
488
+ : {
489
+ summary,
490
+ siblings: selectedRow!.markets.map((market) => ({
491
+ key: market.key,
492
+ marketId: market.marketId,
493
+ label: market.marketLabel,
494
+ yesPrice: market.yesPrice,
495
+ volume24h: market.volume24h,
496
+ })),
497
+ rules: [],
498
+ history: [],
499
+ book: {
500
+ yesBids: [{ price: 0.47, size: 120 }],
501
+ yesAsks: [{ price: 0.49, size: 95 }],
502
+ noBids: [{ price: 0.51, size: 80 }],
503
+ noAsks: [{ price: 0.53, size: 105 }],
504
+ lastTradePrice: null,
505
+ },
506
+ trades: [{
507
+ id: "trade-1",
508
+ timestamp: Date.parse("2026-04-01T12:34:56Z"),
509
+ side: "buy",
510
+ outcome: "yes",
511
+ price: 0.48,
512
+ size: 50,
513
+ }],
514
+ }
515
+ }
498
516
  detailError={null}
499
- detailLoadCount={0}
500
- detailTab="overview"
517
+ detailLoadCount={loading ? 1 : 0}
518
+ detailTab={detailTab}
501
519
  detailWidth={58}
502
520
  focused
503
521
  height={24}
@@ -20,7 +20,7 @@ import { resolvePaneBodyFrame } from "../../../components/layout/pane/sizing";
20
20
  import { getPaneDisplayTitle } from "../../../components/layout/pane/title";
21
21
  import type { AppConfig } from "../../../types/config";
22
22
  import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
23
- import type { TickerFinancials } from "../../../types/financials";
23
+ import type { OptionsChain, TickerFinancials } from "../../../types/financials";
24
24
  import type { TickerRecord } from "../../../types/ticker";
25
25
  import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
26
26
  import type { PaneDef } from "../../../types/plugin";
@@ -32,6 +32,7 @@ interface CliPaneShotPayload {
32
32
  heightCells: number;
33
33
  tickers: TickerRecord[];
34
34
  financials: Array<[string, TickerFinancials]>;
35
+ optionsChains: Array<[string, OptionsChain]>;
35
36
  paneState: Record<string, PaneRuntimeState>;
36
37
  }
37
38
 
@@ -56,6 +57,7 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
56
57
 
57
58
  let pendingShotWork = 0;
58
59
  let didInstallShotFetchTracker = false;
60
+ let shotDataProvider: DataProvider | null = null;
59
61
 
60
62
  const rendererHost: RendererHost = {
61
63
  requestExit() {},
@@ -152,6 +154,7 @@ function waitForShotReadiness(): () => void {
152
154
 
153
155
  function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
154
156
  const financials = new Map(payload.financials.map(([symbol, data]) => [normalizeSymbol(symbol), data]));
157
+ const optionsChains = new Map((payload.optionsChains ?? []).map(([symbol, data]) => [normalizeSymbol(symbol), data]));
155
158
 
156
159
  const getFinancials = (symbol: string) => {
157
160
  const data = financials.get(normalizeSymbol(symbol));
@@ -187,6 +190,13 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
187
190
  getExchangeRate() {
188
191
  return resolveShotWork(1);
189
192
  },
193
+ getOptionsChain(ticker) {
194
+ return trackShotWork(Promise.resolve().then(() => {
195
+ const chain = optionsChains.get(normalizeSymbol(ticker));
196
+ if (!chain) throw new Error(`No screenshot options data available for ${ticker}.`);
197
+ return chain;
198
+ }));
199
+ },
190
200
  search(query) {
191
201
  const normalized = normalizeSymbol(query);
192
202
  return resolveShotWork(payload.tickers
@@ -217,6 +227,7 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
217
227
 
218
228
  function installShotMarketData(payload: CliPaneShotPayload): void {
219
229
  const provider = createShotDataProvider(payload);
230
+ shotDataProvider = provider;
220
231
  const coordinator = new MarketDataCoordinator(provider);
221
232
  coordinator.primeCachedFinancials(payload.tickers.flatMap((ticker) => {
222
233
  const financials = payload.financials.find(([symbol]) => normalizeSymbol(symbol) === normalizeSymbol(ticker.metadata.ticker))?.[1];
@@ -236,7 +247,7 @@ function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
236
247
  return (payload.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? null;
237
248
  }
238
249
  return {
239
- getMarketData: () => null,
250
+ getMarketData: () => shotDataProvider,
240
251
  getCapability: () => null,
241
252
  getBrokerAdapter: () => null,
242
253
  connectBrokerInstance: async () => {},
@@ -53,12 +53,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
53
53
  scrollRef,
54
54
  syncHeaderScroll: _syncHeaderScroll,
55
55
  onBodyScrollActivity,
56
- hoveredIdx,
57
- setHoveredIdx,
58
56
  getItemKey,
59
57
  isSelected,
60
58
  onSelect,
61
59
  onActivate,
60
+ onTableMouseDown,
62
61
  onRowMouseDown,
63
62
  onRowContextMenu,
64
63
  rowContextMenuSurface = false,
@@ -71,6 +70,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
71
70
  emptyStateHint,
72
71
  virtualize = true,
73
72
  overscan = 3,
73
+ columnGap = 1,
74
+ horizontalPadding = 1,
75
+ fillAvailableWidth = true,
74
76
  showHorizontalScrollbar = true,
75
77
  scrollToIndex,
76
78
  scrollToIndexAlign = "nearest",
@@ -85,10 +87,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
85
87
  const bodyVertical = useScrollbarState(true);
86
88
  const [viewportWidth, setViewportWidth] = useState(0);
87
89
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
88
- const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
90
+ const tableWidth = useMemo(
91
+ () => getTableWidth(columns, columnGap, horizontalPadding),
92
+ [columnGap, columns, horizontalPadding],
93
+ );
89
94
  const gridTemplateColumns = useMemo(
90
- () => buildTableGridTemplateColumns(columns),
91
- [columns],
95
+ () => buildTableGridTemplateColumns(columns, fillAvailableWidth),
96
+ [columns, fillAvailableWidth],
92
97
  );
93
98
  const selectRow = useCallback((item: T, index: number) => {
94
99
  onSelect(item, index);
@@ -136,8 +141,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
136
141
  : WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
137
142
  const bodyAfterHeight = bodyAfter ? WEB_CELL_HEIGHT * 6 : 0;
138
143
  const horizontalScrollEnabled = showHorizontalScrollbar
139
- && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
144
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth, columnGap);
140
145
  const scrollContentWidth = horizontalScrollEnabled
146
+ || !fillAvailableWidth
141
147
  ? Math.max(1, tableWidth * WEB_CELL_WIDTH)
142
148
  : "100%";
143
149
  const measureViewportWidth = useCallback(() => {
@@ -247,9 +253,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
247
253
  style={bodyScrollerStyle}
248
254
  onMouseDown={() => {
249
255
  focusPane();
250
- }}
251
- onMouseLeave={() => {
252
- if (hoveredIdx !== null) setHoveredIdx(null);
256
+ onTableMouseDown?.({});
253
257
  }}
254
258
  onScroll={() => {
255
259
  markScrollbarActive();
@@ -270,7 +274,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
270
274
  >
271
275
  <WebDataTableHeader
272
276
  columns={columns}
277
+ columnGap={columnGap}
278
+ horizontalPadding={horizontalPadding}
273
279
  focusPane={focusPane}
280
+ onTableMouseDown={onTableMouseDown}
274
281
  gridTemplateColumns={gridTemplateColumns}
275
282
  onHeaderClick={onHeaderClick}
276
283
  sortColumnId={sortColumnId}
@@ -303,7 +310,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
303
310
  const item = items[row.index];
304
311
  if (!item) return null;
305
312
  const selected = isSelected(item, row.index);
306
- const hovered = hoveredIdx === row.index && !selected;
307
313
  const itemKey = getItemKey(item, row.index);
308
314
  return (
309
315
  <WebDataTableRow<T, C>
@@ -314,19 +320,20 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
314
320
  item={item}
315
321
  itemKey={itemKey}
316
322
  columns={columns}
323
+ columnGap={columnGap}
324
+ horizontalPadding={horizontalPadding}
317
325
  focusPane={focusPane}
326
+ onTableMouseDown={onTableMouseDown}
318
327
  gridTemplateColumns={gridTemplateColumns}
319
328
  onActivateRow={onActivate ? activateRow : undefined}
320
329
  onRowContextMenu={onRowContextMenu}
321
330
  onRowMouseDown={onRowMouseDown}
322
331
  onSelectRow={selectRow}
323
- hovered={hovered}
324
332
  getRowBackgroundColor={getRowBackgroundColor}
325
333
  renderCell={renderCell}
326
334
  renderSectionHeader={renderSectionHeader}
327
335
  rowContextMenuSurface={rowContextMenuSurface}
328
336
  selected={selected}
329
- setHoveredIdx={setHoveredIdx}
330
337
  />
331
338
  );
332
339
  }),
@@ -12,7 +12,6 @@ import type {
12
12
  import { WEB_CELL_HEIGHT } from "../input-host";
13
13
  import {
14
14
  CSS_BG,
15
- CSS_HOVER_BG,
16
15
  CSS_PANEL,
17
16
  CSS_SELECTED,
18
17
  CSS_SELECTED_TEXT,
@@ -44,16 +43,30 @@ function contentJustifyForAlign(align: string | undefined): CSSProperties["justi
44
43
  return "flex-start";
45
44
  }
46
45
 
46
+ function columnGapCss(columnGap: number): CSSProperties["columnGap"] {
47
+ return columnGap === 0 ? 0 : `calc(${columnGap} * var(--cell-w))`;
48
+ }
49
+
50
+ function inlinePaddingPx(horizontalPadding: number): number {
51
+ return TABLE_INLINE_PADDING_PX * horizontalPadding;
52
+ }
53
+
47
54
  export function WebDataTableHeader<C extends DataTableColumn>({
48
55
  columns,
56
+ columnGap,
57
+ horizontalPadding,
49
58
  focusPane,
59
+ onTableMouseDown,
50
60
  gridTemplateColumns,
51
61
  onHeaderClick,
52
62
  sortColumnId,
53
63
  sortDirection,
54
64
  }: {
55
65
  columns: C[];
66
+ columnGap: number;
67
+ horizontalPadding: number;
56
68
  focusPane: () => void;
69
+ onTableMouseDown?: (event: any) => void;
57
70
  gridTemplateColumns: string;
58
71
  onHeaderClick: (columnId: string) => void;
59
72
  sortColumnId: string | null;
@@ -69,13 +82,13 @@ export function WebDataTableHeader<C extends DataTableColumn>({
69
82
  zIndex: 2,
70
83
  display: "grid",
71
84
  gridTemplateColumns,
72
- columnGap: "var(--cell-w)",
85
+ columnGap: columnGapCss(columnGap),
73
86
  alignItems: "center",
74
87
  width: "100%",
75
88
  minWidth: 0,
76
89
  height: WEB_CELL_HEIGHT,
77
- paddingLeft: TABLE_INLINE_PADDING_PX,
78
- paddingRight: TABLE_INLINE_PADDING_PX,
90
+ paddingLeft: inlinePaddingPx(horizontalPadding),
91
+ paddingRight: inlinePaddingPx(horizontalPadding),
79
92
  boxSizing: "border-box",
80
93
  backgroundColor: CSS_PANEL,
81
94
  }}
@@ -99,6 +112,7 @@ export function WebDataTableHeader<C extends DataTableColumn>({
99
112
  }}
100
113
  onMouseDown={(event) => {
101
114
  focusPane();
115
+ onTableMouseDown?.(event);
102
116
  event.preventDefault();
103
117
  onHeaderClick(column.id);
104
118
  }}
@@ -128,12 +142,14 @@ function WebDataTableRowInner<
128
142
  C extends DataTableColumn,
129
143
  >({
130
144
  columns,
145
+ columnGap,
146
+ horizontalPadding,
131
147
  focusPane,
148
+ onTableMouseDown,
132
149
  onActivateRow,
133
150
  onRowContextMenu,
134
151
  onRowMouseDown,
135
152
  onSelectRow,
136
- hovered,
137
153
  index,
138
154
  item,
139
155
  itemKey,
@@ -145,15 +161,16 @@ function WebDataTableRowInner<
145
161
  rowStart,
146
162
  rowContextMenuSurface,
147
163
  selected,
148
- setHoveredIdx,
149
164
  }: {
150
165
  columns: C[];
166
+ columnGap: number;
167
+ horizontalPadding: number;
151
168
  focusPane: () => void;
169
+ onTableMouseDown?: (event: any) => void;
152
170
  onActivateRow?: (item: T, index: number) => void;
153
171
  onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
154
172
  onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
155
173
  onSelectRow: (item: T, index: number) => void;
156
- hovered: boolean;
157
174
  index: number;
158
175
  item: T;
159
176
  itemKey: string;
@@ -165,7 +182,6 @@ function WebDataTableRowInner<
165
182
  rowStart: number;
166
183
  rowContextMenuSurface: boolean;
167
184
  selected: boolean;
168
- setHoveredIdx: (index: number | null) => void;
169
185
  }) {
170
186
  useThemeColors();
171
187
  const sectionHeader: DataTableSectionHeader | null =
@@ -177,13 +193,13 @@ function WebDataTableRowInner<
177
193
  transform: `translateY(${rowStart}px)`,
178
194
  display: "grid",
179
195
  gridTemplateColumns,
180
- columnGap: "var(--cell-w)",
196
+ columnGap: columnGapCss(columnGap),
181
197
  alignItems: "center",
182
198
  width: "100%",
183
199
  minWidth: 0,
184
200
  height: rowSize,
185
- paddingLeft: TABLE_INLINE_PADDING_PX,
186
- paddingRight: TABLE_INLINE_PADDING_PX,
201
+ paddingLeft: inlinePaddingPx(horizontalPadding),
202
+ paddingRight: inlinePaddingPx(horizontalPadding),
187
203
  boxSizing: "border-box",
188
204
  lineHeight: "var(--cell-h)",
189
205
  };
@@ -199,6 +215,7 @@ function WebDataTableRowInner<
199
215
  }}
200
216
  onMouseDown={(event) => {
201
217
  focusPane();
218
+ onTableMouseDown?.(event);
202
219
  event.preventDefault();
203
220
  }}
204
221
  >
@@ -220,13 +237,11 @@ function WebDataTableRowInner<
220
237
  );
221
238
  }
222
239
 
223
- const rowState = { selected, hovered };
240
+ const rowState = { selected };
224
241
  const rowBackgroundColor = getRowBackgroundColor?.(item, index, rowState);
225
242
  const rowBg = selected
226
243
  ? CSS_SELECTED
227
- : hovered
228
- ? CSS_HOVER_BG
229
- : rowBackgroundColor ?? CSS_BG;
244
+ : rowBackgroundColor ?? CSS_BG;
230
245
 
231
246
  return (
232
247
  <div
@@ -238,11 +253,9 @@ function WebDataTableRowInner<
238
253
  ...baseRowStyle,
239
254
  backgroundColor: rowBg,
240
255
  }}
241
- onMouseMove={() => {
242
- if (!hovered) setHoveredIdx(index);
243
- }}
244
256
  onMouseDown={(event) => {
245
257
  focusPane();
258
+ onTableMouseDown?.(event);
246
259
  if (onRowMouseDown?.(item, index, eventWithCellCoordinates(event)) === true) {
247
260
  return;
248
261
  }
@@ -274,6 +287,7 @@ function WebDataTableRowInner<
274
287
  }}
275
288
  onMouseDown={(event) => {
276
289
  focusPane();
290
+ onTableMouseDown?.(event);
277
291
  if (cell.onMouseDown) {
278
292
  cell.onMouseDown(eventWithCellCoordinates(event));
279
293
  return;