gloomberb 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/package.json +2 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -14,7 +14,7 @@ import {
14
14
  import { useQuoteStreaming } from "../../../state/use-quote-streaming";
15
15
  import { getCollectionName, getCollectionTickerCount } from "../../../state/selectors";
16
16
  import { getSharedRegistry } from "../../registry";
17
- import { EmptyState, Tabs } from "../../../components";
17
+ import { EmptyState, PaneFooterScope, Tabs } from "../../../components";
18
18
  import { resolveOptionsTarget } from "../../../utils/options";
19
19
  import { ChartTab } from "./chart-tab";
20
20
  import { ResolvedFinancialsTab } from "./financials-tab";
@@ -210,12 +210,14 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
210
210
  height={contentHeight}
211
211
  overflow="hidden"
212
212
  >
213
- <OverviewTab
214
- width={width}
215
- symbol={symbol}
216
- ticker={ticker}
217
- financials={financials}
218
- />
213
+ <PaneFooterScope active={resolvedTabId === "overview"}>
214
+ <OverviewTab
215
+ width={width}
216
+ symbol={symbol}
217
+ ticker={ticker}
218
+ financials={financials}
219
+ />
220
+ </PaneFooterScope>
219
221
  </Box>
220
222
  )}
221
223
  {renderedTabIds.has("financials") && (
@@ -228,10 +230,12 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
228
230
  height={contentHeight}
229
231
  overflow="hidden"
230
232
  >
231
- <ResolvedFinancialsTab
232
- focused={focused && resolvedTabId === "financials"}
233
- financials={financials}
234
- />
233
+ <PaneFooterScope active={resolvedTabId === "financials"}>
234
+ <ResolvedFinancialsTab
235
+ focused={focused && resolvedTabId === "financials"}
236
+ financials={financials}
237
+ />
238
+ </PaneFooterScope>
235
239
  </Box>
236
240
  )}
237
241
  {renderedTabIds.has("chart") && (
@@ -244,17 +248,19 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
244
248
  height={contentHeight}
245
249
  overflow="hidden"
246
250
  >
247
- <ChartTab
248
- width={width}
249
- height={contentHeight}
250
- focused={focused && resolvedTabId === "chart"}
251
- interactive={chartInteractive}
252
- axisMode={paneSettings.chartAxisMode}
253
- onActivate={() => setChartInteractiveEager(true)}
254
- symbol={symbol}
255
- ticker={ticker}
256
- financials={financials}
257
- />
251
+ <PaneFooterScope active={resolvedTabId === "chart"}>
252
+ <ChartTab
253
+ width={width}
254
+ height={contentHeight}
255
+ focused={focused && resolvedTabId === "chart"}
256
+ interactive={chartInteractive}
257
+ axisMode={paneSettings.chartAxisMode}
258
+ onActivate={() => setChartInteractiveEager(true)}
259
+ symbol={symbol}
260
+ ticker={ticker}
261
+ financials={financials}
262
+ />
263
+ </PaneFooterScope>
258
264
  </Box>
259
265
  )}
260
266
 
@@ -272,12 +278,14 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
272
278
  height={contentHeight}
273
279
  overflow="hidden"
274
280
  >
275
- <PluginTab
276
- width={width}
277
- height={contentHeight}
278
- focused={focused && isActive}
279
- onCapture={isActive ? handlePluginCapture : ignorePluginCapture}
280
- />
281
+ <PaneFooterScope active={isActive}>
282
+ <PluginTab
283
+ width={width}
284
+ height={contentHeight}
285
+ focused={focused && isActive}
286
+ onCapture={isActive ? handlePluginCapture : ignorePluginCapture}
287
+ />
288
+ </PaneFooterScope>
281
289
  </Box>
282
290
  );
283
291
  })}
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { createTestRenderer } from "@opentui/core/testing";
3
- import { createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
4
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
+ import { TestDialogProvider, createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
5
4
  import { act, useReducer, type ReactElement } from "react";
6
5
  import {
7
6
  AppContext,
@@ -65,6 +64,15 @@ function makeFinancials(length: number): TickerFinancials {
65
64
  };
66
65
  }
67
66
 
67
+ function makeFinancialsWithStatements(length: number): TickerFinancials {
68
+ const financials = makeFinancials(length);
69
+ financials.annualStatements = [
70
+ { date: "2023-12-31", totalRevenue: 90, netIncome: 10 },
71
+ { date: "2024-12-31", totalRevenue: 110, netIncome: 14 },
72
+ ];
73
+ return financials;
74
+ }
75
+
68
76
  function makeDetailConfig(symbol: string): AppConfig {
69
77
  const config = createDefaultConfig("/tmp/gloomberb-test");
70
78
  config.chartPreferences.renderer = "kitty";
@@ -102,7 +110,7 @@ function DetailHarness({
102
110
  harnessDispatch = dispatch;
103
111
 
104
112
  return (
105
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
113
+ <TestDialogProvider>
106
114
  <AppContext value={{ state, dispatch }}>
107
115
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
108
116
  <PluginRenderProvider pluginId={tickerDetailPlugin.id} runtime={runtime}>
@@ -116,7 +124,7 @@ function DetailHarness({
116
124
  </PluginRenderProvider>
117
125
  </PaneInstanceProvider>
118
126
  </AppContext>
119
- </DialogProvider>
127
+ </TestDialogProvider>
120
128
  );
121
129
  }
122
130
 
@@ -130,6 +138,13 @@ async function flushFrames(count = 3) {
130
138
  }
131
139
  }
132
140
 
141
+ function getSurfaceVisibleRect(
142
+ manager: { surfaces: Map<string, { snapshot: { visibleRect: unknown } }> },
143
+ surfaceId: string,
144
+ ): unknown {
145
+ return manager.surfaces.get(surfaceId)?.snapshot.visibleRect ?? null;
146
+ }
147
+
133
148
  afterEach(() => {
134
149
  harnessDispatch = null;
135
150
  if (root) {
@@ -201,4 +216,54 @@ describe("Ticker detail chart tab switching", () => {
201
216
  expect(returnedOverview).toContain("AAPL");
202
217
  expect(manager.surfaces.has("chart-surface:ticker-detail:test:compact:base")).toBe(true);
203
218
  });
219
+
220
+ test("hides the full chart kitty surface when switching to financials", async () => {
221
+ const symbol = "AAPL";
222
+ const config = makeDetailConfig(symbol);
223
+
224
+ actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
225
+ testSetup = await createTestRenderer({ width: 120, height: 36 });
226
+ (testSetup.renderer as { _capabilities: unknown })._capabilities = { kitty_graphics: true };
227
+ (testSetup.renderer as { _resolution: unknown })._resolution = { width: 1200, height: 960 };
228
+
229
+ root = createRoot(testSetup.renderer);
230
+ act(() => {
231
+ root!.render(
232
+ <DetailHarness
233
+ config={config}
234
+ ticker={makeTicker(symbol)}
235
+ financials={makeFinancialsWithStatements(48)}
236
+ />,
237
+ );
238
+ });
239
+
240
+ await flushFrames();
241
+ const manager = getNativeSurfaceManager(testSetup.renderer as never) as unknown as {
242
+ surfaces: Map<string, { snapshot: { visibleRect: unknown } }>;
243
+ };
244
+ const fullSurfaceId = "chart-surface:ticker-detail:test:full:base";
245
+
246
+ act(() => {
247
+ harnessDispatch!({
248
+ type: "UPDATE_PANE_STATE",
249
+ paneId: TEST_PANE_ID,
250
+ patch: { activeTabId: "chart" },
251
+ });
252
+ });
253
+
254
+ await flushFrames();
255
+ expect(getSurfaceVisibleRect(manager, fullSurfaceId)).not.toBeNull();
256
+
257
+ act(() => {
258
+ harnessDispatch!({
259
+ type: "UPDATE_PANE_STATE",
260
+ paneId: TEST_PANE_ID,
261
+ patch: { activeTabId: "financials" },
262
+ });
263
+ });
264
+
265
+ await flushFrames();
266
+ expect(testSetup.captureCharFrame()).toContain("Income");
267
+ expect(getSurfaceVisibleRect(manager, fullSurfaceId)).toBeNull();
268
+ });
204
269
  });
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useReducer, type ReactElement } from "react";
3
3
  import { testRender } from "../../renderers/opentui/test-utils";
4
4
  import type { ScrollBoxRenderable } from "@opentui/core";
5
+ import { Box } from "../../ui";
5
6
  import {
6
7
  AppContext,
7
8
  appReducer,
@@ -22,6 +23,7 @@ import type { DataProvider } from "../../types/data-provider";
22
23
  import type { TickerFinancials } from "../../types/financials";
23
24
  import type { DetailTabDef } from "../../types/plugin";
24
25
  import type { TickerRecord } from "../../types/ticker";
26
+ import { PaneFooterBar, PaneFooterProvider, usePaneFooter } from "../../components/layout/pane-footer";
25
27
  import type { PluginRegistry } from "../registry";
26
28
  import { setSharedRegistryForTests } from "../registry";
27
29
  import { PluginRenderProvider } from "../plugin-runtime";
@@ -153,6 +155,34 @@ function makeRegistry(): PluginRegistry {
153
155
  return { detailTabs } as unknown as PluginRegistry;
154
156
  }
155
157
 
158
+ function createFooterProbeTab(tabId: string, label: string): DetailTabDef["component"] {
159
+ return function FooterProbeTab() {
160
+ usePaneFooter(`footer-probe:${tabId}`, () => ({
161
+ info: [{ id: `${tabId}:info`, parts: [{ text: `${tabId}-footer`, tone: "value" }] }],
162
+ hints: [{ id: `${tabId}:hint`, key: label[0]!.toLowerCase(), label: `${label.slice(1).toLowerCase()}-hint` }],
163
+ }), []);
164
+ return <text>{`${label} body`}</text>;
165
+ };
166
+ }
167
+
168
+ function makeFooterRegistry(): PluginRegistry {
169
+ const detailTabs = new Map<string, DetailTabDef>([
170
+ ["probe-alpha", {
171
+ id: "probe-alpha",
172
+ name: "Alpha",
173
+ order: 25,
174
+ component: createFooterProbeTab("alpha", "Alpha"),
175
+ }],
176
+ ["probe-beta", {
177
+ id: "probe-beta",
178
+ name: "Beta",
179
+ order: 30,
180
+ component: createFooterProbeTab("beta", "Beta"),
181
+ }],
182
+ ]);
183
+ return { detailTabs } as unknown as PluginRegistry;
184
+ }
185
+
156
186
  function createGatewayInstance(id = "ibkr-paper"): BrokerInstanceConfig {
157
187
  return {
158
188
  id,
@@ -241,6 +271,40 @@ function DetailHarness({
241
271
  );
242
272
  }
243
273
 
274
+ function DetailFooterHarness({
275
+ config,
276
+ ticker,
277
+ financials,
278
+ activeTabId,
279
+ width = 90,
280
+ height = 18,
281
+ }: {
282
+ config: AppConfig;
283
+ ticker: TickerRecord;
284
+ financials: TickerFinancials | null;
285
+ activeTabId: string;
286
+ width?: number;
287
+ height?: number;
288
+ }) {
289
+ return (
290
+ <PaneFooterProvider>
291
+ {(footer) => (
292
+ <Box flexDirection="column" width={width} height={height}>
293
+ <DetailHarness
294
+ config={config}
295
+ ticker={ticker}
296
+ financials={financials}
297
+ activeTabId={activeTabId}
298
+ width={width}
299
+ height={height - 1}
300
+ />
301
+ <PaneFooterBar footer={footer} focused width={width} />
302
+ </Box>
303
+ )}
304
+ </PaneFooterProvider>
305
+ );
306
+ }
307
+
244
308
  async function flushFrame() {
245
309
  await act(async () => {
246
310
  await testSetup!.renderOnce();
@@ -745,4 +809,42 @@ describe("TickerDetailPane", () => {
745
809
  expect(frame).toContain("active:overview");
746
810
  expect(frame).not.toContain("Trade");
747
811
  });
812
+
813
+ test("shows only active mounted tab pane hints", async () => {
814
+ setSharedRegistryForTests(makeFooterRegistry());
815
+ setOptionsProvider(createProvider(false));
816
+
817
+ testSetup = await testRender(
818
+ <DetailFooterHarness
819
+ config={createDetailConfig("AAPL")}
820
+ ticker={makeTicker("AAPL")}
821
+ financials={null}
822
+ activeTabId="probe-alpha"
823
+ />,
824
+ { width: 90, height: 18 },
825
+ );
826
+
827
+ await flushFrame();
828
+ await flushFrame();
829
+
830
+ let frame = testSetup.captureCharFrame();
831
+ expect(frame).toContain("alpha-footer");
832
+ expect(frame).toContain("[a]lpha-hint");
833
+ expect(frame).not.toContain("beta-footer");
834
+ expect(frame).not.toContain("[b]eta-hint");
835
+
836
+ await act(async () => {
837
+ harnessDispatch!({ type: "UPDATE_PANE_STATE", paneId: TEST_PANE_ID, patch: { activeTabId: "probe-beta" } });
838
+ await Promise.resolve();
839
+ });
840
+ await flushFrame();
841
+ await flushFrame();
842
+
843
+ frame = testSetup.captureCharFrame();
844
+ expect(frame).toContain("Beta body");
845
+ expect(frame).toContain("beta-footer");
846
+ expect(frame).toContain("[b]eta-hint");
847
+ expect(frame).not.toContain("alpha-footer");
848
+ expect(frame).not.toContain("[a]lpha-hint");
849
+ });
748
850
  });
@@ -148,7 +148,7 @@ function nextSortPreference(
148
148
  return DEFAULT_SORT_PREFERENCE;
149
149
  }
150
150
 
151
- export function WorldIndicesPane({ focused, width, height }: PaneProps) {
151
+ function WorldIndicesPane({ focused, width, height }: PaneProps) {
152
152
  const dataProvider = useAssetData();
153
153
  const { pinTicker } = usePluginTickerActions();
154
154
  const [quotes, setQuotes] = useState<QuoteMap>(new Map());
@@ -30,7 +30,7 @@ function spreadBp(points: YieldPoint[]): number | null {
30
30
  return Math.round((y10 - y2) * 100);
31
31
  }
32
32
 
33
- export function YieldCurvePane({ focused, width, height }: PaneProps) {
33
+ function YieldCurvePane({ focused, width, height }: PaneProps) {
34
34
  const [points, setPoints] = useState<YieldPoint[]>([]);
35
35
  const [loading, setLoading] = useState(false);
36
36
  const [error, setError] = useState<string | null>(null);
@@ -1,8 +1,6 @@
1
1
  import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
2
- import type { BrokerInstanceConfig } from "../../types/config";
3
2
  import {
4
3
  buildIbkrConfigFromValues,
5
- getGatewayConfig,
6
4
  IBKR_CONFIG_FIELDS,
7
5
  isFlexConfigured,
8
6
  isGatewayConfigured,
@@ -82,7 +80,7 @@ export const ibkrBroker: BrokerAdapter = {
82
80
  return [{
83
81
  id: "ibkr-console",
84
82
  label: "IBKR Console",
85
- widgetId: "ibkr-trading",
83
+ paneId: "ibkr-trading",
86
84
  disabled: normalized.connectionMode !== "gateway",
87
85
  disabledReason: "IBKR Console is available for Gateway / TWS profiles.",
88
86
  }];
@@ -32,7 +32,7 @@ import {
32
32
  } from "../../components/chart/chart-resolution";
33
33
  import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
34
34
  import type { QuoteSubscriptionTarget } from "../../types/data-provider";
35
- import type { Fundamentals, FinancialStatement, Quote, PricePoint, TickerFinancials } from "../../types/financials";
35
+ import type { Quote, PricePoint, TickerFinancials } from "../../types/financials";
36
36
  import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
37
37
  import type {
38
38
  BrokerAccount,
@@ -658,10 +658,6 @@ function buildConnectionNote(
658
658
  return notes.length > 0 ? notes.join(" ") : undefined;
659
659
  }
660
660
 
661
- function isRetryableErrorCode(code: number): boolean {
662
- return ![200, 201, 202, 321, 354].includes(code);
663
- }
664
-
665
661
  function isLoopbackHost(host: string): boolean {
666
662
  return LOCAL_IBKR_HOSTS.has(host.trim().toLowerCase());
667
663
  }
@@ -1,7 +1,7 @@
1
1
  import type { TimeRange } from "../../components/chart/chart-types";
2
2
  import type { ChartResolutionSupport, ManualChartResolution } from "../../components/chart/chart-resolution";
3
3
  import { getBrokerRemoteClient } from "../../brokers/remote-broker-adapter";
4
- import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
4
+ import type { BrokerPosition } from "../../types/broker";
5
5
  import type { QuoteSubscriptionTarget } from "../../types/data-provider";
6
6
  import type { Quote, PricePoint, TickerFinancials } from "../../types/financials";
7
7
  import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
@@ -110,8 +110,6 @@ function setupIbkrPlugin(config: AppConfig) {
110
110
  pinTicker: () => {},
111
111
  on: () => () => {},
112
112
  emit: () => {},
113
- showWidget: () => {},
114
- hideWidget: () => {},
115
113
  notify: ({ body }: { body: string }) => { toasts.push(body); },
116
114
  } satisfies GloomPluginContext;
117
115
 
@@ -1,7 +1,8 @@
1
1
  import { afterEach, expect, test } from "bun:test";
2
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
- import { testRender } from "../../renderers/opentui/test-utils";
2
+ import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
4
3
  import { act } from "react";
4
+ import { Box } from "../../ui";
5
+ import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
5
6
  import {
6
7
  AppContext,
7
8
  PaneInstanceProvider,
@@ -104,26 +105,40 @@ function TradeHarness({
104
105
  ticker,
105
106
  financials,
106
107
  brokerAccounts = {},
108
+ withFooter = false,
107
109
  }: {
108
110
  config: AppConfig;
109
111
  ticker: TickerRecord;
110
112
  financials: TickerFinancials;
111
113
  brokerAccounts?: Record<string, import("../../types/trading").BrokerAccount[]>;
114
+ withFooter?: boolean;
112
115
  }) {
113
116
  const state = createInitialState(config);
114
117
  state.focusedPaneId = TEST_PANE_ID;
115
118
  state.tickers = new Map([[ticker.metadata.ticker, ticker]]);
116
119
  state.financials = new Map([[ticker.metadata.ticker, financials]]);
117
120
  state.brokerAccounts = brokerAccounts;
121
+ const tradeTab = <TradeTab focused width={88} height={30} onCapture={() => {}} />;
118
122
 
119
123
  return (
120
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
124
+ <TestDialogProvider>
121
125
  <AppContext value={{ state, dispatch: () => {} }}>
122
126
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
123
- <TradeTab focused width={88} height={30} onCapture={() => {}} />
127
+ {withFooter
128
+ ? (
129
+ <PaneFooterProvider>
130
+ {(footer) => (
131
+ <Box flexDirection="column" width={88} height={31}>
132
+ {tradeTab}
133
+ <PaneFooterBar footer={footer} focused width={88} />
134
+ </Box>
135
+ )}
136
+ </PaneFooterProvider>
137
+ )
138
+ : tradeTab}
124
139
  </PaneInstanceProvider>
125
140
  </AppContext>
126
- </DialogProvider>
141
+ </TestDialogProvider>
127
142
  );
128
143
  }
129
144
 
@@ -223,3 +238,39 @@ test("prefills the only cached IBKR account when the live gateway snapshot is em
223
238
  expect(frame).toContain("Account DU123456");
224
239
  expect(frame).toContain("Paper Gateway");
225
240
  });
241
+
242
+ test("omits redundant and disabled trade footer hints", async () => {
243
+ const config = createTradeConfig("AMD");
244
+ const ticker = makeTicker("AMD", "Advanced Micro Devices, Inc.");
245
+ const financials = makeFinancials();
246
+
247
+ clearTradingDraft();
248
+ stubGatewayRefresh();
249
+ prefillTradeFromTicker(ticker, "BUY");
250
+
251
+ await act(async () => {
252
+ testSetup = await testRender(
253
+ <TradeHarness config={config} ticker={ticker} financials={financials} withFooter />,
254
+ { width: 88, height: 31 },
255
+ );
256
+ });
257
+
258
+ await act(async () => {
259
+ await testSetup!.renderOnce();
260
+ });
261
+ await act(async () => {
262
+ await testSetup!.renderOnce();
263
+ });
264
+
265
+ const frame = testSetup.captureCharFrame();
266
+ expect(frame).toContain("[r]efresh");
267
+ expect(frame).toContain("[i]profile");
268
+ expect(frame).toContain("[a]ccount");
269
+ expect(frame).toContain("[b/v]side");
270
+ expect(frame).toContain("[q]ty");
271
+ expect(frame).toContain("[t]ype");
272
+ expect(frame).toContain("[p]review");
273
+ expect(frame).not.toContain("[s]ymbol");
274
+ expect(frame).not.toContain("[l]imit");
275
+ expect(frame).not.toContain("[x]stop");
276
+ });
@@ -695,21 +695,23 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
695
695
  ...(statusText ? [{ id: "status", parts: [{ text: statusText, tone: statusTone === "negative" ? "negative" as const : statusTone === "positive" ? "positive" as const : "muted" as const }] }] : []),
696
696
  ],
697
697
  hints: [
698
- { id: "refresh", key: "r", label: "efresh", onPress: () => refresh().catch(() => {}), disabled: ticketState.busy },
699
- { id: "profile", key: "i", label: "profile", onPress: () => chooseBrokerInstance().catch(() => {}), disabled: ticketState.busy },
700
- { id: "instrument", key: "s", label: "ymbol", onPress: () => chooseInstrument().catch(() => {}), disabled: ticketState.busy },
701
- { id: "account", key: "a", label: "ccount", onPress: () => chooseAccount().catch(() => {}), disabled: ticketState.busy },
702
- { id: "side", key: "b/v", label: "side", onPress: symbol && ticker ? () => setTradeTicketDraft(symbol, { action: ticketState.draft.action === "BUY" ? "SELL" : "BUY" }, ticker) : undefined, disabled: ticketState.busy || !symbol || !ticker },
703
- { id: "quantity", key: "q", label: "ty", onPress: () => editNumericField("Quantity", ticketState.draft.quantity, (value) => { if (value != null && symbol && ticker) setTradeTicketDraft(symbol, { quantity: value }, ticker); }).catch(() => {}), disabled: ticketState.busy || !symbol || !ticker },
704
- { id: "type", key: "t", label: "ype", onPress: () => editOrderType().catch(() => {}), disabled: ticketState.busy },
705
- { id: "limit", key: "l", label: "imit", onPress: () => editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => { if (symbol && ticker) setTradeTicketDraft(symbol, { limitPrice: value }, ticker); }).catch(() => {}), disabled: ticketState.busy || !showLimit || !symbol || !ticker },
706
- { id: "stop", key: "x", label: "stop", onPress: () => editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => { if (symbol && ticker) setTradeTicketDraft(symbol, { stopPrice: value }, ticker); }).catch(() => {}), disabled: ticketState.busy || !showStop || !symbol || !ticker },
707
- { id: "preview", key: "p", label: "review", onPress: () => previewOrder().catch(() => {}), disabled: ticketState.busy || !symbol || !ticker },
698
+ ...(!ticketState.busy ? [
699
+ { id: "refresh", key: "r", label: "efresh", onPress: () => refresh().catch(() => {}) },
700
+ { id: "profile", key: "i", label: "profile", onPress: () => chooseBrokerInstance().catch(() => {}) },
701
+ { id: "account", key: "a", label: "ccount", onPress: () => chooseAccount().catch(() => {}) },
702
+ ] : []),
703
+ ...(!ticketState.busy && symbol && ticker ? [
704
+ { id: "side", key: "b/v", label: "side", onPress: () => setTradeTicketDraft(symbol, { action: ticketState.draft.action === "BUY" ? "SELL" : "BUY" }, ticker) },
705
+ { id: "quantity", key: "q", label: "ty", onPress: () => editNumericField("Quantity", ticketState.draft.quantity, (value) => { if (value != null) setTradeTicketDraft(symbol, { quantity: value }, ticker); }).catch(() => {}) },
706
+ { id: "type", key: "t", label: "ype", onPress: () => editOrderType().catch(() => {}) },
707
+ ...(showLimit ? [{ id: "limit", key: "l", label: "imit", onPress: () => editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => { setTradeTicketDraft(symbol, { limitPrice: value }, ticker); }).catch(() => {}) }] : []),
708
+ ...(showStop ? [{ id: "stop", key: "x", label: "stop", onPress: () => editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => { setTradeTicketDraft(symbol, { stopPrice: value }, ticker); }).catch(() => {}) }] : []),
709
+ { id: "preview", key: "p", label: "review", onPress: () => previewOrder().catch(() => {}) },
710
+ ] : []),
708
711
  ],
709
712
  }), [
710
713
  chooseAccount,
711
714
  chooseBrokerInstance,
712
- chooseInstrument,
713
715
  editNumericField,
714
716
  editOrderType,
715
717
  editPriceField,
@@ -96,19 +96,6 @@ function clampRatio(ratio: number): number {
96
96
  return Math.max(0.1, Math.min(0.9, ratio));
97
97
  }
98
98
 
99
- function cloneDockNode(node: DockLayoutNode): DockLayoutNode {
100
- if (node.kind === "pane") {
101
- return { kind: "pane", instanceId: node.instanceId };
102
- }
103
- return {
104
- kind: "split",
105
- axis: node.axis,
106
- ratio: node.ratio,
107
- first: cloneDockNode(node.first),
108
- second: cloneDockNode(node.second),
109
- };
110
- }
111
-
112
99
  function ensurePaneInstance(layout: LayoutConfig, instance: PaneInstanceConfig): LayoutConfig {
113
100
  if (layout.instances.some((entry) => entry.instanceId === instance.instanceId)) return layout;
114
101
  return { ...layout, instances: [...layout.instances, instance] };
@@ -199,11 +199,11 @@ describe("plugin runtime hooks", () => {
199
199
  openCommandBar(query?: string) {
200
200
  calls.push(`command:${query ?? ""}`);
201
201
  },
202
- showWidget(widgetId: string) {
203
- calls.push(`show:${widgetId}`);
202
+ showPane(paneId: string) {
203
+ calls.push(`show:${paneId}`);
204
204
  },
205
- hideWidget(widgetId: string) {
206
- calls.push(`hide:${widgetId}`);
205
+ hidePane(paneId: string) {
206
+ calls.push(`hide:${paneId}`);
207
207
  },
208
208
  openPluginCommandWorkflow(commandId: string) {
209
209
  calls.push(`workflow:${commandId}`);
@@ -230,8 +230,8 @@ describe("plugin runtime hooks", () => {
230
230
  });
231
231
 
232
232
  actions?.openCommandBar("PL ");
233
- actions?.showWidget("debug");
234
- actions?.hideWidget("chat");
233
+ actions?.showPane("debug");
234
+ actions?.hidePane("chat");
235
235
  actions?.openPluginCommandWorkflow("set-alert");
236
236
  actions?.notify({ body: "Saved", type: "success" });
237
237
 
@@ -35,8 +35,9 @@ export interface PluginRuntimeAccess {
35
35
  switchTab(tabId: string, paneId?: string): void;
36
36
  switchPanel(panel: "left" | "right"): void;
37
37
  openCommandBar(query?: string): void;
38
- showWidget(widgetId: string): void;
39
- hideWidget(widgetId: string): void;
38
+ showPane(paneId: string): void;
39
+ hidePane(paneId: string): void;
40
+ openPaneSettings(paneId?: string): void;
40
41
  openPluginCommandWorkflow(commandId: string): void;
41
42
  notify(notification: AppNotificationRequest): void;
42
43
  subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
@@ -114,11 +115,14 @@ export function usePluginAppActions() {
114
115
  const openCommandBar = useCallback((query?: string) => {
115
116
  runtime.openCommandBar(query);
116
117
  }, [runtime]);
117
- const showWidget = useCallback((widgetId: string) => {
118
- runtime.showWidget(widgetId);
118
+ const showPane = useCallback((paneId: string) => {
119
+ runtime.showPane(paneId);
119
120
  }, [runtime]);
120
- const hideWidget = useCallback((widgetId: string) => {
121
- runtime.hideWidget(widgetId);
121
+ const hidePane = useCallback((paneId: string) => {
122
+ runtime.hidePane(paneId);
123
+ }, [runtime]);
124
+ const openPaneSettings = useCallback((paneId?: string) => {
125
+ runtime.openPaneSettings(paneId);
122
126
  }, [runtime]);
123
127
  const openPluginCommandWorkflow = useCallback((commandId: string) => {
124
128
  runtime.openPluginCommandWorkflow(commandId);
@@ -129,8 +133,9 @@ export function usePluginAppActions() {
129
133
 
130
134
  return {
131
135
  openCommandBar,
132
- showWidget,
133
- hideWidget,
136
+ showPane,
137
+ hidePane,
138
+ openPaneSettings,
134
139
  openPluginCommandWorkflow,
135
140
  notify,
136
141
  };
@@ -1,4 +1,3 @@
1
- import { Input } from "../../ui";
2
1
  import type { CliCommandDef } from "../../types/plugin";
3
2
  import { createPredictionLaunchRequest, parsePredictionCommandArgs } from "./launch";
4
3
 
@@ -37,8 +37,6 @@ import type {
37
37
  PredictionVenueScope,
38
38
  } from "./types";
39
39
 
40
- const KEYBOARD_DETAIL_LOAD_DELAY_MS = 140;
41
-
42
40
  function formatPredictionVenueLabel(venue: PredictionVenue): string {
43
41
  return venue === "polymarket" ? "Polymarket" : "Kalshi";
44
42
  }