gloomberb 0.9.3 → 0.9.4

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 (137) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/app/runtime/desktop-menu.ts +5 -1
  4. package/src/app/runtime/ticker-refresh.ts +4 -1
  5. package/src/app.tsx +10 -1
  6. package/src/brokers/account-cache.ts +1 -1
  7. package/src/brokers/sync-broker-instance.test.ts +107 -0
  8. package/src/brokers/sync-broker-instance.ts +22 -10
  9. package/src/cli/desktop-pane-shot.ts +2 -1
  10. package/src/cli/pane-functions/screenshot.ts +13 -1
  11. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  12. package/src/components/chart/bar-chart-renderer.ts +226 -27
  13. package/src/components/chart/core/pointer.ts +32 -6
  14. package/src/components/chart/price-axis-labels.tsx +3 -1
  15. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  16. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  17. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  18. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  19. package/src/components/chart/static/chart/surface.tsx +167 -24
  20. package/src/components/chart/stock-chart/index.test.ts +25 -0
  21. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  22. package/src/components/layout/detached-pane-shell.tsx +6 -3
  23. package/src/components/layout/floating-pane.tsx +3 -2
  24. package/src/components/layout/pane/content.tsx +20 -8
  25. package/src/components/layout/pane/footer/registration.tsx +6 -2
  26. package/src/components/layout/pane/frame.tsx +4 -4
  27. package/src/components/layout/pane/header.tsx +40 -4
  28. package/src/components/layout/pane/index.tsx +3 -1
  29. package/src/components/layout/pane/sizing.test.ts +43 -0
  30. package/src/components/layout/pane/sizing.ts +13 -4
  31. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  32. package/src/components/layout/shell/drag/runtime.ts +9 -2
  33. package/src/components/layout/shell/index.test.tsx +92 -2
  34. package/src/components/layout/shell/index.tsx +37 -2
  35. package/src/components/layout/shell/layout-state.test.ts +33 -0
  36. package/src/components/layout/shell/layout-state.ts +4 -9
  37. package/src/components/layout/shell/pane/layers.tsx +8 -2
  38. package/src/components/layout/shell/visible-layout.ts +17 -0
  39. package/src/components/layout/status-bar.test.tsx +6 -1
  40. package/src/components/layout/status-bar.tsx +5 -1
  41. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  42. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  43. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  44. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  45. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  46. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  47. package/src/components/table-view-shared.tsx +12 -4
  48. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  49. package/src/components/ui/data-table/types.ts +4 -0
  50. package/src/components/ui/fields.tsx +10 -3
  51. package/src/components/ui/page-stack-view.tsx +6 -2
  52. package/src/components/ui/table-layout.test.ts +25 -0
  53. package/src/components/ui/table-layout.ts +22 -6
  54. package/src/components/ui/tabs.tsx +26 -10
  55. package/src/components/ui/ui.test.tsx +87 -0
  56. package/src/core/state/app/layout-reducer.ts +2 -2
  57. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  58. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  59. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  60. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  61. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  62. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  63. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  64. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  65. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  66. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  67. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  68. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  69. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  70. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  71. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  72. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  73. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  74. package/src/plugins/builtin/options/view.test.tsx +141 -4
  75. package/src/plugins/builtin/options/view.tsx +76 -15
  76. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  77. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  78. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  79. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  80. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  81. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  82. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  83. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  84. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  85. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  86. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  87. package/src/plugins/catalog-ui.ts +2 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  89. package/src/plugins/ibkr/config.test.ts +31 -2
  90. package/src/plugins/ibkr/config.ts +9 -2
  91. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  92. package/src/plugins/ibkr/flex/index.ts +30 -0
  93. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  94. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  95. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  96. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  97. package/src/plugins/pane-manager/gridlock.ts +13 -5
  98. package/src/plugins/pane-manager/layout-state.ts +25 -0
  99. package/src/plugins/pane-manager.test.ts +33 -0
  100. package/src/plugins/pane-manager.ts +6 -1
  101. package/src/plugins/prediction-markets/chart.tsx +10 -4
  102. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  103. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  104. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  105. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  106. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  107. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  108. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  109. package/src/plugins/prediction-markets/pane.tsx +50 -22
  110. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  111. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  112. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  113. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  114. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  115. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  116. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  117. package/src/renderers/electrobun/view/styles.css +11 -0
  118. package/src/renderers/opentui/host.tsx +17 -1
  119. package/src/renderers/opentui/ui-host.tsx +57 -25
  120. package/src/sources/provider-router/batches.ts +16 -2
  121. package/src/sources/provider-router/financial-routes.ts +5 -0
  122. package/src/sources/provider-router/financials.ts +11 -23
  123. package/src/sources/provider-router/index.test.ts +189 -0
  124. package/src/sources/provider-router/primary.ts +3 -2
  125. package/src/sources/sec-edgar.test.ts +83 -0
  126. package/src/sources/sec-edgar.ts +231 -0
  127. package/src/sources/yahoo-finance.test.ts +51 -0
  128. package/src/sources/yahoo-finance.ts +49 -1
  129. package/src/state/app/context/index.test.tsx +5 -8
  130. package/src/types/broker.ts +1 -0
  131. package/src/types/config.ts +10 -0
  132. package/src/types/trading.ts +2 -0
  133. package/src/ui/host.tsx +16 -0
  134. package/src/ui/index.tsx +2 -0
  135. package/src/utils/financial-statements.ts +23 -0
  136. package/src/version.ts +1 -1
  137. 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 () => {},
@@ -59,6 +59,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
59
59
  isSelected,
60
60
  onSelect,
61
61
  onActivate,
62
+ onTableMouseDown,
62
63
  onRowMouseDown,
63
64
  onRowContextMenu,
64
65
  rowContextMenuSurface = false,
@@ -71,6 +72,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
71
72
  emptyStateHint,
72
73
  virtualize = true,
73
74
  overscan = 3,
75
+ columnGap = 1,
76
+ horizontalPadding = 1,
77
+ fillAvailableWidth = true,
74
78
  showHorizontalScrollbar = true,
75
79
  scrollToIndex,
76
80
  scrollToIndexAlign = "nearest",
@@ -85,10 +89,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
85
89
  const bodyVertical = useScrollbarState(true);
86
90
  const [viewportWidth, setViewportWidth] = useState(0);
87
91
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
88
- const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
92
+ const tableWidth = useMemo(
93
+ () => getTableWidth(columns, columnGap, horizontalPadding),
94
+ [columnGap, columns, horizontalPadding],
95
+ );
89
96
  const gridTemplateColumns = useMemo(
90
- () => buildTableGridTemplateColumns(columns),
91
- [columns],
97
+ () => buildTableGridTemplateColumns(columns, fillAvailableWidth),
98
+ [columns, fillAvailableWidth],
92
99
  );
93
100
  const selectRow = useCallback((item: T, index: number) => {
94
101
  onSelect(item, index);
@@ -136,8 +143,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
136
143
  : WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
137
144
  const bodyAfterHeight = bodyAfter ? WEB_CELL_HEIGHT * 6 : 0;
138
145
  const horizontalScrollEnabled = showHorizontalScrollbar
139
- && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
146
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth, columnGap);
140
147
  const scrollContentWidth = horizontalScrollEnabled
148
+ || !fillAvailableWidth
141
149
  ? Math.max(1, tableWidth * WEB_CELL_WIDTH)
142
150
  : "100%";
143
151
  const measureViewportWidth = useCallback(() => {
@@ -247,6 +255,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
247
255
  style={bodyScrollerStyle}
248
256
  onMouseDown={() => {
249
257
  focusPane();
258
+ onTableMouseDown?.({});
250
259
  }}
251
260
  onMouseLeave={() => {
252
261
  if (hoveredIdx !== null) setHoveredIdx(null);
@@ -270,7 +279,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
270
279
  >
271
280
  <WebDataTableHeader
272
281
  columns={columns}
282
+ columnGap={columnGap}
283
+ horizontalPadding={horizontalPadding}
273
284
  focusPane={focusPane}
285
+ onTableMouseDown={onTableMouseDown}
274
286
  gridTemplateColumns={gridTemplateColumns}
275
287
  onHeaderClick={onHeaderClick}
276
288
  sortColumnId={sortColumnId}
@@ -314,7 +326,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
314
326
  item={item}
315
327
  itemKey={itemKey}
316
328
  columns={columns}
329
+ columnGap={columnGap}
330
+ horizontalPadding={horizontalPadding}
317
331
  focusPane={focusPane}
332
+ onTableMouseDown={onTableMouseDown}
318
333
  gridTemplateColumns={gridTemplateColumns}
319
334
  onActivateRow={onActivate ? activateRow : undefined}
320
335
  onRowContextMenu={onRowContextMenu}
@@ -44,16 +44,30 @@ function contentJustifyForAlign(align: string | undefined): CSSProperties["justi
44
44
  return "flex-start";
45
45
  }
46
46
 
47
+ function columnGapCss(columnGap: number): CSSProperties["columnGap"] {
48
+ return columnGap === 0 ? 0 : `calc(${columnGap} * var(--cell-w))`;
49
+ }
50
+
51
+ function inlinePaddingPx(horizontalPadding: number): number {
52
+ return TABLE_INLINE_PADDING_PX * horizontalPadding;
53
+ }
54
+
47
55
  export function WebDataTableHeader<C extends DataTableColumn>({
48
56
  columns,
57
+ columnGap,
58
+ horizontalPadding,
49
59
  focusPane,
60
+ onTableMouseDown,
50
61
  gridTemplateColumns,
51
62
  onHeaderClick,
52
63
  sortColumnId,
53
64
  sortDirection,
54
65
  }: {
55
66
  columns: C[];
67
+ columnGap: number;
68
+ horizontalPadding: number;
56
69
  focusPane: () => void;
70
+ onTableMouseDown?: (event: any) => void;
57
71
  gridTemplateColumns: string;
58
72
  onHeaderClick: (columnId: string) => void;
59
73
  sortColumnId: string | null;
@@ -69,13 +83,13 @@ export function WebDataTableHeader<C extends DataTableColumn>({
69
83
  zIndex: 2,
70
84
  display: "grid",
71
85
  gridTemplateColumns,
72
- columnGap: "var(--cell-w)",
86
+ columnGap: columnGapCss(columnGap),
73
87
  alignItems: "center",
74
88
  width: "100%",
75
89
  minWidth: 0,
76
90
  height: WEB_CELL_HEIGHT,
77
- paddingLeft: TABLE_INLINE_PADDING_PX,
78
- paddingRight: TABLE_INLINE_PADDING_PX,
91
+ paddingLeft: inlinePaddingPx(horizontalPadding),
92
+ paddingRight: inlinePaddingPx(horizontalPadding),
79
93
  boxSizing: "border-box",
80
94
  backgroundColor: CSS_PANEL,
81
95
  }}
@@ -99,6 +113,7 @@ export function WebDataTableHeader<C extends DataTableColumn>({
99
113
  }}
100
114
  onMouseDown={(event) => {
101
115
  focusPane();
116
+ onTableMouseDown?.(event);
102
117
  event.preventDefault();
103
118
  onHeaderClick(column.id);
104
119
  }}
@@ -128,7 +143,10 @@ function WebDataTableRowInner<
128
143
  C extends DataTableColumn,
129
144
  >({
130
145
  columns,
146
+ columnGap,
147
+ horizontalPadding,
131
148
  focusPane,
149
+ onTableMouseDown,
132
150
  onActivateRow,
133
151
  onRowContextMenu,
134
152
  onRowMouseDown,
@@ -148,7 +166,10 @@ function WebDataTableRowInner<
148
166
  setHoveredIdx,
149
167
  }: {
150
168
  columns: C[];
169
+ columnGap: number;
170
+ horizontalPadding: number;
151
171
  focusPane: () => void;
172
+ onTableMouseDown?: (event: any) => void;
152
173
  onActivateRow?: (item: T, index: number) => void;
153
174
  onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
154
175
  onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
@@ -177,13 +198,13 @@ function WebDataTableRowInner<
177
198
  transform: `translateY(${rowStart}px)`,
178
199
  display: "grid",
179
200
  gridTemplateColumns,
180
- columnGap: "var(--cell-w)",
201
+ columnGap: columnGapCss(columnGap),
181
202
  alignItems: "center",
182
203
  width: "100%",
183
204
  minWidth: 0,
184
205
  height: rowSize,
185
- paddingLeft: TABLE_INLINE_PADDING_PX,
186
- paddingRight: TABLE_INLINE_PADDING_PX,
206
+ paddingLeft: inlinePaddingPx(horizontalPadding),
207
+ paddingRight: inlinePaddingPx(horizontalPadding),
187
208
  boxSizing: "border-box",
188
209
  lineHeight: "var(--cell-h)",
189
210
  };
@@ -199,6 +220,7 @@ function WebDataTableRowInner<
199
220
  }}
200
221
  onMouseDown={(event) => {
201
222
  focusPane();
223
+ onTableMouseDown?.(event);
202
224
  event.preventDefault();
203
225
  }}
204
226
  >
@@ -243,6 +265,7 @@ function WebDataTableRowInner<
243
265
  }}
244
266
  onMouseDown={(event) => {
245
267
  focusPane();
268
+ onTableMouseDown?.(event);
246
269
  if (onRowMouseDown?.(item, index, eventWithCellCoordinates(event)) === true) {
247
270
  return;
248
271
  }
@@ -274,6 +297,7 @@ function WebDataTableRowInner<
274
297
  }}
275
298
  onMouseDown={(event) => {
276
299
  focusPane();
300
+ onTableMouseDown?.(event);
277
301
  if (cell.onMouseDown) {
278
302
  cell.onMouseDown(eventWithCellCoordinates(event));
279
303
  return;
@@ -84,6 +84,7 @@ export function WebTextField({
84
84
  inputRef,
85
85
  onChange,
86
86
  onSubmit,
87
+ onBlur,
87
88
  hint,
88
89
  type = "text",
89
90
  variant = "default",
@@ -155,7 +156,12 @@ export function WebTextField({
155
156
  }}
156
157
  onInput={onChange}
157
158
  onChange={onChange}
158
- onSubmit={() => onSubmit?.(value ?? resolvedInputRef.current?.editBuffer.getText() ?? "")}
159
+ onSubmit={(nextValue?: string) => onSubmit?.(
160
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
161
+ )}
162
+ onBlur={(nextValue?: string) => onBlur?.(
163
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
164
+ )}
159
165
  />
160
166
  </Box>
161
167
  {hint && (
@@ -347,14 +353,14 @@ export function WebPageStackView({
347
353
 
348
354
  if (!detailOpen) {
349
355
  return (
350
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
356
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
351
357
  {rootContent}
352
358
  </Box>
353
359
  );
354
360
  }
355
361
 
356
362
  return (
357
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
363
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
358
364
  <Box
359
365
  flexDirection="row"
360
366
  alignItems="flex-start"
@@ -370,7 +376,7 @@ export function WebPageStackView({
370
376
  flexDirection="row"
371
377
  alignItems="center"
372
378
  backgroundColor={panelFill()}
373
- onMouseDown={(event) => {
379
+ onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
374
380
  event.preventDefault?.();
375
381
  event.stopPropagation?.();
376
382
  onBack();
@@ -413,7 +419,7 @@ export function WebPageStackView({
413
419
  </Text>
414
420
  ) : null}
415
421
  </Box>
416
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
422
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
417
423
  {detailContent}
418
424
  </Box>
419
425
  </Box>