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.
- package/package.json +2 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +70 -0
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +17 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +478 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- 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
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
<
|
|
124
|
+
<TestDialogProvider>
|
|
121
125
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
122
126
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
123
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
203
|
-
calls.push(`show:${
|
|
202
|
+
showPane(paneId: string) {
|
|
203
|
+
calls.push(`show:${paneId}`);
|
|
204
204
|
},
|
|
205
|
-
|
|
206
|
-
calls.push(`hide:${
|
|
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?.
|
|
234
|
-
actions?.
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
118
|
-
runtime.
|
|
118
|
+
const showPane = useCallback((paneId: string) => {
|
|
119
|
+
runtime.showPane(paneId);
|
|
119
120
|
}, [runtime]);
|
|
120
|
-
const
|
|
121
|
-
runtime.
|
|
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
|
-
|
|
133
|
-
|
|
136
|
+
showPane,
|
|
137
|
+
hidePane,
|
|
138
|
+
openPaneSettings,
|
|
134
139
|
openPluginCommandWorkflow,
|
|
135
140
|
notify,
|
|
136
141
|
};
|