gloomberb 0.9.2 → 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 (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -148,6 +148,7 @@ export function summarizeBrokerAccount(
148
148
  source: "gateway",
149
149
  updatedAt,
150
150
  netLiquidation: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "NetLiquidation", currency, allowAggregateCashBalances),
151
+ grossPositionValue: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "GrossPositionValue", currency, allowAggregateCashBalances),
151
152
  totalCashValue: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "TotalCashValue", currency, allowAggregateCashBalances),
152
153
  settledCash: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "SettledCash", currency, allowAggregateCashBalances),
153
154
  availableFunds: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "AvailableFunds", currency, allowAggregateCashBalances),
@@ -35,6 +35,7 @@ describe("summarizeBrokerAccount", () => {
35
35
  test("maps gateway account summary fields and ledger balances", () => {
36
36
  const tags = makeTags({
37
37
  NetLiquidation: { USD: "764713.62" },
38
+ GrossPositionValue: { USD: "1815667.34" },
38
39
  TotalCashValue: { USD: "-1050953.72" },
39
40
  SettledCash: { USD: "-917604.44" },
40
41
  AvailableFunds: { USD: "112345.67" },
@@ -52,6 +53,7 @@ describe("summarizeBrokerAccount", () => {
52
53
  source: "gateway",
53
54
  updatedAt: 1_717_000_000_000,
54
55
  netLiquidation: 764713.62,
56
+ grossPositionValue: 1815667.34,
55
57
  totalCashValue: -1050953.72,
56
58
  settledCash: -917604.44,
57
59
  availableFunds: 112345.67,
@@ -72,6 +74,7 @@ describe("summarizeBrokerAccount", () => {
72
74
  test("falls back to the aggregate cash summary for single-account gateways", () => {
73
75
  const accountTags = makeTags({
74
76
  NetLiquidation: { USD: "129360.15" },
77
+ GrossPositionValue: { USD: "1066.36" },
75
78
  TotalCashValue: { USD: "128293.79" },
76
79
  AvailableFunds: { USD: "129031.52" },
77
80
  BuyingPower: { USD: "860210.13" },
@@ -99,6 +102,7 @@ describe("summarizeBrokerAccount", () => {
99
102
  source: "gateway",
100
103
  updatedAt: 1_717_000_000_000,
101
104
  netLiquidation: 129360.15,
105
+ grossPositionValue: 1066.36,
102
106
  totalCashValue: 128293.79,
103
107
  settledCash: undefined,
104
108
  availableFunds: 129031.52,
@@ -119,6 +123,7 @@ describe("summarizeBrokerAccount", () => {
119
123
  test("falls back to aggregate summary metrics when a single-account gateway only returns the All row", () => {
120
124
  const aggregateTags = makeTags({
121
125
  NetLiquidation: { USD: "129360.15" },
126
+ GrossPositionValue: { USD: "1066.36" },
122
127
  TotalCashValue: { USD: "128293.79" },
123
128
  AvailableFunds: { USD: "129031.52" },
124
129
  BuyingPower: { USD: "860210.13" },
@@ -144,6 +149,7 @@ describe("summarizeBrokerAccount", () => {
144
149
  source: "gateway",
145
150
  updatedAt: 1_717_000_000_000,
146
151
  netLiquidation: 129360.15,
152
+ grossPositionValue: 1066.36,
147
153
  totalCashValue: 128293.79,
148
154
  settledCash: undefined,
149
155
  availableFunds: 129031.52,
@@ -164,6 +170,7 @@ describe("summarizeBrokerAccount", () => {
164
170
  test("maps account-level P&L into gateway account snapshots", () => {
165
171
  const tags = makeTags({
166
172
  NetLiquidation: { USD: "100000" },
173
+ GrossPositionValue: { USD: "75000" },
167
174
  });
168
175
 
169
176
  expect(summarizeBrokerAccount("DU12345", tags, 1_717_000_000_000, undefined, false, {
@@ -186,6 +193,7 @@ describe("summarizeBrokerAccount", () => {
186
193
  source: "gateway",
187
194
  updatedAt: 123,
188
195
  netLiquidation: undefined,
196
+ grossPositionValue: undefined,
189
197
  totalCashValue: undefined,
190
198
  settledCash: undefined,
191
199
  availableFunds: undefined,
@@ -6,6 +6,7 @@ import {
6
6
  findDockLeaf,
7
7
  getNodeAtPath,
8
8
  replaceNodeAtPath,
9
+ type DockGeometryOptions,
9
10
  type DockTarget,
10
11
  type LayoutBounds,
11
12
  } from "./dock-tree";
@@ -116,11 +117,17 @@ export function applyDrop(layout: LayoutConfig, draggedId: string, dropTarget: D
116
117
  return insertRelativeToLeaf(layout, draggedId, dropTarget.targetId, direction);
117
118
  }
118
119
 
119
- export function simulateDrop(layout: LayoutConfig, draggedId: string, dropTarget: DropTarget, bounds: LayoutBounds): LayoutSimulation {
120
+ export function simulateDrop(
121
+ layout: LayoutConfig,
122
+ draggedId: string,
123
+ dropTarget: DropTarget,
124
+ bounds: LayoutBounds,
125
+ options?: DockGeometryOptions,
126
+ ): LayoutSimulation {
120
127
  const nextLayout = applyDrop(layout, draggedId, dropTarget);
121
128
  return {
122
129
  layout: nextLayout,
123
- previewRect: getLeafRect(nextLayout, draggedId, bounds),
130
+ previewRect: getLeafRect(nextLayout, draggedId, bounds, options),
124
131
  };
125
132
  }
126
133
 
@@ -8,15 +8,23 @@ import {
8
8
  getDockLeafLayouts,
9
9
  type LayoutBounds,
10
10
  } from "./dock-tree";
11
- import { finalizeLayout } from "./layout-state";
11
+ import {
12
+ finalizeLayout,
13
+ removeUnavailablePaneTypes,
14
+ type PaneTypeAvailability,
15
+ } from "./layout-state";
12
16
 
13
17
  export function gridlockAllPanes(
14
18
  layout: LayoutConfig,
15
19
  bounds: LayoutBounds = { x: 0, y: 0, width: 120, height: 40 },
20
+ paneTypes?: PaneTypeAvailability,
16
21
  ): LayoutConfig {
17
- const dockedRects: GridlockRect[] = getDockLeafLayouts(layout, bounds)
22
+ const visibleLayout = paneTypes
23
+ ? removeUnavailablePaneTypes(layout, paneTypes)
24
+ : layout;
25
+ const dockedRects: GridlockRect[] = getDockLeafLayouts(visibleLayout, bounds)
18
26
  .map((leaf) => ({ instanceId: leaf.instanceId, ...leaf.rect }));
19
- const floatingRects: GridlockRect[] = layout.floating.map((entry) => ({
27
+ const floatingRects: GridlockRect[] = visibleLayout.floating.map((entry) => ({
20
28
  instanceId: entry.instanceId,
21
29
  x: entry.x,
22
30
  y: entry.y,
@@ -24,10 +32,10 @@ export function gridlockAllPanes(
24
32
  height: entry.height,
25
33
  }));
26
34
  const allRects = [...dockedRects, ...floatingRects];
27
- if (allRects.length === 0) return layout;
35
+ if (allRects.length === 0) return visibleLayout;
28
36
 
29
37
  return finalizeLayout({
30
- ...layout,
38
+ ...visibleLayout,
31
39
  dockRoot: inferDockTreeFromRects(allRects, boundsForRects(allRects)),
32
40
  floating: [],
33
41
  });
@@ -169,3 +169,28 @@ export function removeFloatingPanes(layout: LayoutConfig): LayoutConfig {
169
169
  layout.floating.map((entry) => entry.instanceId),
170
170
  ));
171
171
  }
172
+
173
+ export type PaneTypeAvailability = { has(paneId: string): boolean } | ((paneId: string) => unknown);
174
+
175
+ function paneTypeIsAvailable(availability: PaneTypeAvailability, paneId: string): boolean {
176
+ return typeof availability === "function"
177
+ ? !!availability(paneId)
178
+ : availability.has(paneId);
179
+ }
180
+
181
+ export function removeUnavailablePaneTypes(
182
+ layout: LayoutConfig,
183
+ availability: PaneTypeAvailability,
184
+ options: { disabledPaneIds?: ReadonlySet<string> } = {},
185
+ ): LayoutConfig {
186
+ const unavailableInstanceIds = layout.instances
187
+ .filter((instance) => (
188
+ options.disabledPaneIds?.has(instance.paneId)
189
+ || !paneTypeIsAvailable(availability, instance.paneId)
190
+ ))
191
+ .map((instance) => instance.instanceId);
192
+
193
+ return unavailableInstanceIds.length > 0
194
+ ? removePaneInstances(layout, unavailableInstanceIds)
195
+ : layout;
196
+ }
@@ -10,6 +10,7 @@ import {
10
10
  countColumnsFromGeometry,
11
11
  findDockLeaf,
12
12
  getDockLeafLayouts,
13
+ type DockGeometryOptions,
13
14
  type LayoutBounds,
14
15
  } from "./dock-tree";
15
16
 
@@ -20,8 +21,13 @@ export interface ResolvedPane {
20
21
  path?: Array<0 | 1>;
21
22
  }
22
23
 
23
- export function getLeafRect(layout: LayoutConfig, instanceId: string, bounds: LayoutBounds): LayoutBounds | null {
24
- return getDockLeafLayouts(layout, bounds).find((entry) => entry.instanceId === instanceId)?.rect ?? null;
24
+ export function getLeafRect(
25
+ layout: LayoutConfig,
26
+ instanceId: string,
27
+ bounds: LayoutBounds,
28
+ options?: DockGeometryOptions,
29
+ ): LayoutBounds | null {
30
+ return getDockLeafLayouts(layout, bounds, options).find((entry) => entry.instanceId === instanceId)?.rect ?? null;
25
31
  }
26
32
 
27
33
  export function resolveDocked(
@@ -109,6 +109,39 @@ describe("pane-manager split-tree drops", () => {
109
109
  expect(getDockedPaneIds(next)).toHaveLength(layout.instances.length);
110
110
  });
111
111
 
112
+ test("gridlock drops pane types that cannot render before tiling", () => {
113
+ const layout: LayoutConfig = {
114
+ dockRoot: {
115
+ kind: "split" as const,
116
+ axis: "horizontal" as const,
117
+ ratio: 0.5,
118
+ first: {
119
+ kind: "split" as const,
120
+ axis: "vertical" as const,
121
+ ratio: 0.5,
122
+ first: { kind: "pane" as const, instanceId: "missing:main" },
123
+ second: { kind: "pane" as const, instanceId: "chat:main" },
124
+ },
125
+ second: { kind: "pane" as const, instanceId: "sectors:main" },
126
+ },
127
+ instances: [
128
+ createPaneInstance("missing-plugin", { instanceId: "missing:main" }),
129
+ createPaneInstance("chat", { instanceId: "chat:main" }),
130
+ createPaneInstance("sectors", { instanceId: "sectors:main" }),
131
+ ],
132
+ floating: [],
133
+ detached: [],
134
+ };
135
+
136
+ const next = gridlockAllPanes(layout, BOUNDS, new Set(["chat", "sectors"]));
137
+
138
+ expect(next.instances.map((instance) => instance.instanceId)).toEqual(["chat:main", "sectors:main"]);
139
+ expect(getDockedPaneIds(next)).toEqual(["chat:main", "sectors:main"]);
140
+ expect(getLeafRect(next, "missing:main", BOUNDS)).toBeNull();
141
+ expect(getLeafRect(next, "chat:main", BOUNDS)).toEqual({ x: 0, y: 0, width: 60, height: 40 });
142
+ expect(getLeafRect(next, "sectors:main", BOUNDS)).toEqual({ x: 60, y: 0, width: 60, height: 40 });
143
+ });
144
+
112
145
  test("gridlock infers a matching tiled layout from arranged windows", () => {
113
146
  const config = createDefaultConfig("/tmp/gloomberb-test");
114
147
  let layout: LayoutConfig = {
@@ -49,7 +49,12 @@ export {
49
49
  swapPanes,
50
50
  } from "./pane-manager/docking";
51
51
 
52
- export { removeFloatingPanes, removePane } from "./pane-manager/layout-state";
52
+ export {
53
+ removeFloatingPanes,
54
+ removePane,
55
+ removeUnavailablePaneTypes,
56
+ type PaneTypeAvailability,
57
+ } from "./pane-manager/layout-state";
53
58
  export { gridlockAllPanes } from "./pane-manager/gridlock";
54
59
  export type {
55
60
  DropTarget,
@@ -72,12 +72,14 @@ export function PredictionMarketChart({
72
72
  history,
73
73
  width,
74
74
  height,
75
+ loading = false,
75
76
  range,
76
77
  onRangeSelect,
77
78
  }: {
78
79
  history: PredictionHistoryPoint[];
79
80
  width: number;
80
81
  height: number;
82
+ loading?: boolean;
81
83
  range: PredictionHistoryRange;
82
84
  onRangeSelect: (range: PredictionHistoryRange) => void;
83
85
  }) {
@@ -93,10 +95,14 @@ export function PredictionMarketChart({
93
95
  />
94
96
  </Box>
95
97
  <Box flexGrow={1} justifyContent="center">
96
- <EmptyState
97
- title="No chart history."
98
- hint="This venue did not return price history for the selected market."
99
- />
98
+ {loading ? (
99
+ <Text fg={colors.textDim}>Loading chart...</Text>
100
+ ) : (
101
+ <EmptyState
102
+ title="No chart history."
103
+ hint="This venue did not return price history for the selected market."
104
+ />
105
+ )}
100
106
  </Box>
101
107
  </Box>
102
108
  );
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { type InputRenderable, type ScrollBoxRenderable } from "../../../ui";
3
3
  import { useViewport } from "../../../react/input";
4
+ import { useAppInputCapture } from "../../../state/app/input-capture";
4
5
  import { usePaneInstance } from "../../../state/app/context";
5
6
  import {
6
7
  useDebouncedPluginPaneState,
@@ -101,6 +102,7 @@ export function usePredictionMarketsController({
101
102
  const [searchFocused, setSearchFocused] = useState(false);
102
103
  const [initialParamsApplied, setInitialParamsApplied] = useState(false);
103
104
  const appViewport = useViewport();
105
+ useAppInputCapture(focused && searchFocused);
104
106
 
105
107
  const scrollRef = useRef<ScrollBoxRenderable>(null);
106
108
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
@@ -1,6 +1,7 @@
1
1
  import { useCallback, type RefObject } from "react";
2
2
  import { type ScrollBoxRenderable } from "../../../ui";
3
3
  import { useShortcut } from "../../../react/input";
4
+ import { isPlainArrowDown, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
4
5
  import { getAdjacentPredictionCategoryId } from "../categories";
5
6
  import { resolvePredictionKeyboardCommand } from "../keyboard";
6
7
  import { getAdjacentPredictionVenueScope } from "../navigation";
@@ -16,7 +17,14 @@ import type {
16
17
  interface PredictionKeyboardEvent {
17
18
  name?: string;
18
19
  sequence?: string;
20
+ ctrl?: boolean;
21
+ meta?: boolean;
22
+ super?: boolean;
23
+ alt?: boolean;
24
+ option?: boolean;
19
25
  shift?: boolean;
26
+ defaultPrevented?: boolean;
27
+ propagationStopped?: boolean;
20
28
  preventDefault?: () => void;
21
29
  stopPropagation?: () => void;
22
30
  }
@@ -62,6 +70,25 @@ export function usePredictionControllerKeyboard({
62
70
  setVenue,
63
71
  toggleWatchlist,
64
72
  }: UsePredictionControllerKeyboardParams) {
73
+ const handleSearchNavigation = useCallback(
74
+ (event: PredictionKeyboardEvent) => {
75
+ if (!focused || !searchFocused) return;
76
+
77
+ if (isPlainArrowDown(event)) {
78
+ stopSearchFocusNavigation(event);
79
+ blurSearch();
80
+ return;
81
+ }
82
+
83
+ if (resolvePredictionKeyboardCommand(event) === "escape") {
84
+ event.stopPropagation?.();
85
+ event.preventDefault?.();
86
+ blurSearch();
87
+ }
88
+ },
89
+ [blurSearch, focused, searchFocused],
90
+ );
91
+
65
92
  const cycleDetailOutcome = useCallback(
66
93
  (direction: "previous" | "next") => {
67
94
  if (detailTab !== "overview" || sortedOutcomeMarkets.length === 0) {
@@ -98,15 +125,11 @@ export function usePredictionControllerKeyboard({
98
125
 
99
126
  const handleKeyboard = useCallback(
100
127
  (event: PredictionKeyboardEvent) => {
128
+ if (event.defaultPrevented || event.propagationStopped) return;
101
129
  if (!focused) return;
102
130
  const command = resolvePredictionKeyboardCommand(event);
103
131
 
104
132
  if (searchFocused) {
105
- if (command === "escape") {
106
- event.stopPropagation?.();
107
- event.preventDefault?.();
108
- blurSearch();
109
- }
110
133
  return;
111
134
  }
112
135
 
@@ -216,5 +239,11 @@ export function usePredictionControllerKeyboard({
216
239
  ],
217
240
  );
218
241
 
242
+ useShortcut(handleSearchNavigation, {
243
+ allowEditable: true,
244
+ enabled: focused && searchFocused,
245
+ phase: "before",
246
+ });
247
+
219
248
  useShortcut(handleKeyboard);
220
249
  }
@@ -1,7 +1,7 @@
1
- import { Box, Text } from "../../../ui";
2
- import { TextAttributes } from "../../../ui";
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { DataTableView, type DataTableColumn } from "../../../components";
3
3
  import { colors } from "../../../theme/colors";
4
- import { formatNumber, padTo } from "../../../utils/format";
4
+ import { formatNumber } from "../../../utils/format";
5
5
  import { formatPredictionProbability } from "../metrics";
6
6
  import type {
7
7
  PredictionBookLevel,
@@ -9,111 +9,156 @@ import type {
9
9
  PredictionOrderPreviewIntent,
10
10
  } from "../types";
11
11
 
12
- function BookTable({
13
- title,
12
+ type BookColumnId = "outcome" | "side" | "price" | "size";
13
+ type BookColumn = DataTableColumn & { id: BookColumnId };
14
+
15
+ interface BookRow {
16
+ id: string;
17
+ outcome: "yes" | "no";
18
+ side: "buy" | "sell";
19
+ level: PredictionBookLevel;
20
+ intent: PredictionOrderPreviewIntent;
21
+ }
22
+
23
+ const BOOK_COLUMNS: BookColumn[] = [
24
+ { id: "outcome", label: "OUT", width: 5 },
25
+ { id: "side", label: "SIDE", width: 6 },
26
+ { id: "price", label: "PRICE", width: 8, align: "right" },
27
+ { id: "size", label: "SIZE", width: 10, align: "right" },
28
+ ];
29
+
30
+ function bookRowsForLevels({
14
31
  levels,
15
- onSelect,
32
+ marketKey,
33
+ outcome,
34
+ side,
16
35
  }: {
17
- title: string;
18
36
  levels: PredictionBookLevel[];
19
- onSelect: (level: PredictionBookLevel) => void;
20
- }) {
21
- return (
22
- <Box flexDirection="column" flexGrow={1}>
23
- <Box height={1}>
24
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
25
- {title}
26
- </Text>
27
- </Box>
28
- <Box height={1}>
29
- <Text fg={colors.textDim}>
30
- {padTo("PRICE", 8)} {padTo("SIZE", 10)}
31
- </Text>
32
- </Box>
33
- {levels.slice(0, 10).map((level) => (
34
- <Box
35
- key={`${title}:${level.price}:${level.size}`}
36
- height={1}
37
- onMouseDown={(event) => {
38
- event.preventDefault();
39
- onSelect(level);
40
- }}
41
- >
42
- <Text fg={colors.text}>
43
- {`${padTo(formatPredictionProbability(level.price), 8)} ${padTo(formatNumber(level.size, 0), 10, "right")}`}
44
- </Text>
45
- </Box>
46
- ))}
47
- {levels.length === 0 && <Text fg={colors.textDim}>No levels.</Text>}
48
- </Box>
49
- );
37
+ marketKey: string;
38
+ outcome: "yes" | "no";
39
+ side: "buy" | "sell";
40
+ }): BookRow[] {
41
+ return levels.slice(0, 10).map((level, index) => ({
42
+ id: `${outcome}:${side}:${index}:${level.price}:${level.size}`,
43
+ outcome,
44
+ side,
45
+ level,
46
+ intent: {
47
+ marketKey,
48
+ outcome,
49
+ side,
50
+ price: level.price,
51
+ size: level.size,
52
+ },
53
+ }));
54
+ }
55
+
56
+ function buildBookRows(detail: PredictionMarketDetail): BookRow[] {
57
+ const marketKey = detail.summary.key;
58
+ return [
59
+ ...bookRowsForLevels({
60
+ levels: detail.book.yesBids,
61
+ marketKey,
62
+ outcome: "yes",
63
+ side: "buy",
64
+ }),
65
+ ...bookRowsForLevels({
66
+ levels: detail.book.yesAsks,
67
+ marketKey,
68
+ outcome: "yes",
69
+ side: "sell",
70
+ }),
71
+ ...bookRowsForLevels({
72
+ levels: detail.book.noBids,
73
+ marketKey,
74
+ outcome: "no",
75
+ side: "buy",
76
+ }),
77
+ ...bookRowsForLevels({
78
+ levels: detail.book.noAsks,
79
+ marketKey,
80
+ outcome: "no",
81
+ side: "sell",
82
+ }),
83
+ ];
50
84
  }
51
85
 
52
86
  export function PredictionMarketBookView({
53
87
  detail,
88
+ focused,
54
89
  onPreviewOrder,
90
+ width,
55
91
  }: {
56
92
  detail: PredictionMarketDetail;
93
+ focused: boolean;
57
94
  onPreviewOrder: (intent: PredictionOrderPreviewIntent) => void;
95
+ width: number;
58
96
  }) {
97
+ const rows = useMemo(() => buildBookRows(detail), [detail]);
98
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(() =>
99
+ rows.length > 0 ? 0 : null,
100
+ );
101
+
102
+ useEffect(() => {
103
+ setSelectedIndex((current) => {
104
+ if (rows.length === 0) return null;
105
+ if (current == null || current >= rows.length) return 0;
106
+ return current;
107
+ });
108
+ }, [rows.length]);
109
+
59
110
  return (
60
- <Box flexDirection="column" gap={1}>
61
- <Box flexDirection="row" gap={2}>
62
- <BookTable
63
- title="YES Bids"
64
- levels={detail.book.yesBids}
65
- onSelect={(level) =>
66
- onPreviewOrder({
67
- marketKey: detail.summary.key,
68
- outcome: "yes",
69
- side: "buy",
70
- price: level.price,
71
- size: level.size,
72
- })
73
- }
74
- />
75
- <BookTable
76
- title="YES Asks"
77
- levels={detail.book.yesAsks}
78
- onSelect={(level) =>
79
- onPreviewOrder({
80
- marketKey: detail.summary.key,
81
- outcome: "yes",
82
- side: "sell",
83
- price: level.price,
84
- size: level.size,
85
- })
86
- }
87
- />
88
- </Box>
89
- <Box flexDirection="row" gap={2}>
90
- <BookTable
91
- title="NO Bids"
92
- levels={detail.book.noBids}
93
- onSelect={(level) =>
94
- onPreviewOrder({
95
- marketKey: detail.summary.key,
96
- outcome: "no",
97
- side: "buy",
98
- price: level.price,
99
- size: level.size,
100
- })
101
- }
102
- />
103
- <BookTable
104
- title="NO Asks"
105
- levels={detail.book.noAsks}
106
- onSelect={(level) =>
107
- onPreviewOrder({
108
- marketKey: detail.summary.key,
109
- outcome: "no",
110
- side: "sell",
111
- price: level.price,
112
- size: level.size,
113
- })
114
- }
115
- />
116
- </Box>
117
- </Box>
111
+ <DataTableView<BookRow, BookColumn>
112
+ focused={focused}
113
+ keyboardNavigation={focused}
114
+ rootWidth={width}
115
+ rootBackgroundColor={colors.panel}
116
+ selection={{
117
+ kind: "index",
118
+ selectedIndex,
119
+ onChange: (index) => setSelectedIndex(index),
120
+ }}
121
+ columns={BOOK_COLUMNS}
122
+ items={rows}
123
+ sortColumnId={null}
124
+ sortDirection="asc"
125
+ onHeaderClick={() => {}}
126
+ getItemKey={(row) => row.id}
127
+ onActivate={(row) => onPreviewOrder(row.intent)}
128
+ onRowMouseDown={(row, index, event) => {
129
+ event.preventDefault();
130
+ setSelectedIndex(index);
131
+ onPreviewOrder(row.intent);
132
+ return true;
133
+ }}
134
+ renderCell={(row, column, _index, rowState) => {
135
+ const color = (fallback: string) =>
136
+ rowState.selected ? undefined : fallback;
137
+ switch (column.id) {
138
+ case "outcome":
139
+ return {
140
+ text: row.outcome.toUpperCase(),
141
+ color: color(colors.textBright),
142
+ };
143
+ case "side":
144
+ return {
145
+ text: row.side === "buy" ? "BID" : "ASK",
146
+ color: color(row.side === "buy" ? colors.positive : colors.negative),
147
+ };
148
+ case "price":
149
+ return {
150
+ text: formatPredictionProbability(row.level.price),
151
+ color: color(colors.text),
152
+ };
153
+ case "size":
154
+ return {
155
+ text: formatNumber(row.level.size, 0),
156
+ color: color(colors.textDim),
157
+ };
158
+ }
159
+ }}
160
+ emptyStateTitle="No book levels."
161
+ emptyStateHint="This venue did not return current order book depth."
162
+ />
118
163
  );
119
164
  }
@@ -17,6 +17,7 @@ export function PredictionMarketOverviewView({
17
17
  detailWidth,
18
18
  height,
19
19
  historyRange,
20
+ loading,
20
21
  onHistoryRangeChange,
21
22
  onSelectMarket,
22
23
  selectedRow,
@@ -26,6 +27,7 @@ export function PredictionMarketOverviewView({
26
27
  detailWidth: number;
27
28
  height: number;
28
29
  historyRange: PredictionHistoryRange;
30
+ loading: boolean;
29
31
  onHistoryRangeChange: (range: PredictionHistoryRange) => void;
30
32
  onSelectMarket: (marketKey: string) => void;
31
33
  selectedRow: PredictionListRow | null;
@@ -47,6 +49,7 @@ export function PredictionMarketOverviewView({
47
49
  history={detail?.history ?? []}
48
50
  width={detailWidth}
49
51
  height={Math.max(Math.floor(height * 0.36), 10)}
52
+ loading={loading}
50
53
  range={historyRange}
51
54
  onRangeSelect={onHistoryRangeChange}
52
55
  />