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
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
5
4
|
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
6
5
|
import { Button } from "./button";
|
|
7
6
|
import { ChoiceDialog } from "./choice-dialog";
|
|
8
|
-
import { DataTable } from "./data-table";
|
|
7
|
+
import { DataTable, type DataTableColumn } from "./data-table";
|
|
9
8
|
import { TextField } from "./fields";
|
|
10
9
|
import { ListView } from "./list-view";
|
|
11
10
|
import { MultiSelectDialogButton } from "./multi-select-dialog";
|
|
@@ -21,12 +20,13 @@ import { Tabs } from "./tabs";
|
|
|
21
20
|
import { ToggleList } from "../toggle-list";
|
|
22
21
|
import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
|
|
23
22
|
import { createDefaultConfig } from "../../types/config";
|
|
23
|
+
import { DataTableView } from "../data-table-view";
|
|
24
24
|
|
|
25
25
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
26
26
|
let setListSelection: ((index: number) => void) | null = null;
|
|
27
27
|
let selectedTableRow: string | null = null;
|
|
28
28
|
let activatedTableRow: string | null = null;
|
|
29
|
-
let
|
|
29
|
+
let tableScrollBoxForTest: ScrollBoxRenderable | null = null;
|
|
30
30
|
let closedTab: string | null = null;
|
|
31
31
|
let addedTab = false;
|
|
32
32
|
let resolvedChoice: string | null = null;
|
|
@@ -125,34 +125,47 @@ function DataTableSectionHarness() {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function
|
|
128
|
+
function DataTableHorizontalScrollHarness({
|
|
129
|
+
columns = [{ id: "name", label: "NAME", width: 12, align: "left" }],
|
|
130
|
+
containerWidth = 32,
|
|
131
|
+
containerHeight = 5,
|
|
132
|
+
rowCount = 1,
|
|
133
|
+
showHorizontalScrollbar,
|
|
134
|
+
}: {
|
|
135
|
+
columns?: DataTableColumn[];
|
|
136
|
+
containerWidth?: number;
|
|
137
|
+
containerHeight?: number;
|
|
138
|
+
rowCount?: number;
|
|
139
|
+
showHorizontalScrollbar?: boolean;
|
|
140
|
+
}) {
|
|
129
141
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
130
142
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
131
|
-
const
|
|
143
|
+
const items = Array.from({ length: rowCount }, (_, index) => ({
|
|
144
|
+
id: `row-${index}`,
|
|
145
|
+
name: index === 0 ? "Alpha" : `Alpha ${index + 1}`,
|
|
146
|
+
}));
|
|
132
147
|
|
|
133
148
|
useEffect(() => {
|
|
134
|
-
|
|
149
|
+
tableScrollBoxForTest = scrollRef.current;
|
|
135
150
|
});
|
|
136
151
|
|
|
137
152
|
return (
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
<DataTableView
|
|
154
|
+
rootWidth={containerWidth}
|
|
155
|
+
rootHeight={containerHeight}
|
|
156
|
+
columns={columns}
|
|
157
|
+
items={items}
|
|
141
158
|
sortColumnId={null}
|
|
142
159
|
sortDirection="asc"
|
|
143
160
|
onHeaderClick={() => {}}
|
|
144
161
|
headerScrollRef={headerScrollRef}
|
|
145
162
|
scrollRef={scrollRef}
|
|
146
|
-
syncHeaderScroll={() => {}}
|
|
147
|
-
onBodyScrollActivity={() => {}}
|
|
148
|
-
hoveredIdx={hoveredIdx}
|
|
149
|
-
setHoveredIdx={setHoveredIdx}
|
|
150
163
|
getItemKey={(row) => row.id}
|
|
151
164
|
isSelected={() => false}
|
|
152
165
|
onSelect={() => {}}
|
|
153
166
|
renderCell={(row) => ({ text: row.name })}
|
|
154
167
|
emptyStateTitle="No rows."
|
|
155
|
-
showHorizontalScrollbar={
|
|
168
|
+
showHorizontalScrollbar={showHorizontalScrollbar}
|
|
156
169
|
/>
|
|
157
170
|
);
|
|
158
171
|
}
|
|
@@ -192,7 +205,7 @@ function MultiSelectDialogButtonHarness() {
|
|
|
192
205
|
const [values, setValues] = useState(["sma"]);
|
|
193
206
|
|
|
194
207
|
return (
|
|
195
|
-
<
|
|
208
|
+
<TestDialogProvider>
|
|
196
209
|
<MultiSelectDialogButton
|
|
197
210
|
label="IND"
|
|
198
211
|
title="Chart Indicators"
|
|
@@ -204,7 +217,7 @@ function MultiSelectDialogButtonHarness() {
|
|
|
204
217
|
{ value: "ema", label: "EMA" },
|
|
205
218
|
]}
|
|
206
219
|
/>
|
|
207
|
-
</
|
|
220
|
+
</TestDialogProvider>
|
|
208
221
|
);
|
|
209
222
|
}
|
|
210
223
|
|
|
@@ -253,7 +266,7 @@ afterEach(() => {
|
|
|
253
266
|
setListSelection = null;
|
|
254
267
|
selectedTableRow = null;
|
|
255
268
|
activatedTableRow = null;
|
|
256
|
-
|
|
269
|
+
tableScrollBoxForTest = null;
|
|
257
270
|
closedTab = null;
|
|
258
271
|
addedTab = false;
|
|
259
272
|
resolvedChoice = null;
|
|
@@ -650,10 +663,77 @@ describe("shared UI kit", () => {
|
|
|
650
663
|
|
|
651
664
|
test("hides data table horizontal scrolling when disabled", async () => {
|
|
652
665
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
666
|
+
tableScrollBoxForTest = null;
|
|
667
|
+
testSetup = await testRender(
|
|
668
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
669
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
670
|
+
<DataTableHorizontalScrollHarness showHorizontalScrollbar={false} />
|
|
671
|
+
</PaneInstanceProvider>
|
|
672
|
+
</AppContext>,
|
|
673
|
+
{ width: 32, height: 5 },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
await act(async () => {
|
|
677
|
+
await testSetup!.renderOnce();
|
|
678
|
+
await testSetup!.renderOnce();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("hides data table horizontal scrolling when content fits", async () => {
|
|
685
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
686
|
+
tableScrollBoxForTest = null;
|
|
687
|
+
testSetup = await testRender(
|
|
688
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
689
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
690
|
+
<DataTableHorizontalScrollHarness />
|
|
691
|
+
</PaneInstanceProvider>
|
|
692
|
+
</AppContext>,
|
|
693
|
+
{ width: 32, height: 5 },
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
await act(async () => {
|
|
697
|
+
await testSetup!.renderOnce();
|
|
698
|
+
await testSetup!.renderOnce();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("shows data table horizontal scrolling when content overflows", async () => {
|
|
705
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
706
|
+
tableScrollBoxForTest = null;
|
|
707
|
+
testSetup = await testRender(
|
|
708
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
709
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
710
|
+
<DataTableHorizontalScrollHarness
|
|
711
|
+
containerWidth={24}
|
|
712
|
+
columns={[
|
|
713
|
+
{ id: "name", label: "NAME", width: 24, align: "left" },
|
|
714
|
+
{ id: "price", label: "PRICE", width: 18, align: "right" },
|
|
715
|
+
]}
|
|
716
|
+
/>
|
|
717
|
+
</PaneInstanceProvider>
|
|
718
|
+
</AppContext>,
|
|
719
|
+
{ width: 24, height: 5 },
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
await act(async () => {
|
|
723
|
+
await testSetup!.renderOnce();
|
|
724
|
+
await testSetup!.renderOnce();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(true);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("shows data table vertical scrolling when rows overflow", async () => {
|
|
731
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
732
|
+
tableScrollBoxForTest = null;
|
|
653
733
|
testSetup = await testRender(
|
|
654
734
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
655
735
|
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
656
|
-
<
|
|
736
|
+
<DataTableHorizontalScrollHarness rowCount={20} containerHeight={5} />
|
|
657
737
|
</PaneInstanceProvider>
|
|
658
738
|
</AppContext>,
|
|
659
739
|
{ width: 32, height: 5 },
|
|
@@ -664,7 +744,7 @@ describe("shared UI kit", () => {
|
|
|
664
744
|
await testSetup!.renderOnce();
|
|
665
745
|
});
|
|
666
746
|
|
|
667
|
-
expect(
|
|
747
|
+
expect(tableScrollBoxForTest?.verticalScrollBar.visible).toBe(true);
|
|
668
748
|
});
|
|
669
749
|
|
|
670
750
|
test("virtualizes data table rows by default", async () => {
|
|
@@ -387,10 +387,12 @@ function removeHistoryIndex(layoutHistory: Record<number, LayoutHistoryEntry>, r
|
|
|
387
387
|
|
|
388
388
|
/** If paneId is a floating pane, bump its zIndex to the top. */
|
|
389
389
|
function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfig {
|
|
390
|
-
const
|
|
390
|
+
const entryIndex = layout.floating.findIndex((e) => e.instanceId === paneId);
|
|
391
|
+
const entry = entryIndex >= 0 ? layout.floating[entryIndex] : undefined;
|
|
391
392
|
if (!entry) return layout;
|
|
392
393
|
const maxZ = layout.floating.reduce((max, e) => Math.max(max, e.zIndex ?? 50), 0);
|
|
393
|
-
|
|
394
|
+
const topEqualIndex = layout.floating.findLastIndex((e) => (e.zIndex ?? 50) === maxZ);
|
|
395
|
+
if ((entry.zIndex ?? 50) === maxZ && entryIndex === topEqualIndex) return layout; // already on top
|
|
394
396
|
return {
|
|
395
397
|
...layout,
|
|
396
398
|
floating: layout.floating.map((e) =>
|
|
@@ -792,7 +794,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
792
794
|
|
|
793
795
|
case "FOCUS_PREV": {
|
|
794
796
|
if (action.paneOrder.length === 0) return state;
|
|
795
|
-
const
|
|
797
|
+
const focusedIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : -1;
|
|
798
|
+
const currentIndex = focusedIndex >= 0 ? focusedIndex : 0;
|
|
796
799
|
const nextPaneId = action.paneOrder[(currentIndex - 1 + action.paneOrder.length) % action.paneOrder.length]!;
|
|
797
800
|
return focusPaneState(state, nextPaneId);
|
|
798
801
|
}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { MarketDataCoordinator } from "./coordinator";
|
|
3
3
|
import type { DataProvider, QuoteSubscriptionTarget } from "../types/data-provider";
|
|
4
4
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
6
6
|
import { createTestDataProvider } from "../test-support/data-provider";
|
|
7
7
|
|
|
8
8
|
function createProvider(overrides: Partial<DataProvider> = {}): DataProvider {
|
|
@@ -4,7 +4,6 @@ import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types
|
|
|
4
4
|
import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "./request-types";
|
|
5
5
|
import { QueryStore } from "./query-store";
|
|
6
6
|
import type { ProviderAttempt, ProviderReasonCode, QueryEntry } from "./result-types";
|
|
7
|
-
import { createIdleEntry } from "./result-types";
|
|
8
7
|
import {
|
|
9
8
|
buildArticleSummaryKey,
|
|
10
9
|
buildChartKey,
|
package/src/news/aggregator.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
|
|
2
|
-
import { useRef, useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useRef, useEffect, useState, useCallback, type SetStateAction } from "react";
|
|
3
3
|
import { useShortcut } from "../../../react/input";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
5
|
import { type ScrollBoxRenderable, type TextareaRenderable } from "../../../ui";
|
|
@@ -33,7 +33,22 @@ const chatHistories = new Map<string, ChatMessage[]>();
|
|
|
33
33
|
export { __setDetectedProvidersForTests };
|
|
34
34
|
export type { AiProvider };
|
|
35
35
|
|
|
36
|
+
function sameMessages(left: readonly ChatMessage[], right: readonly ChatMessage[]): boolean {
|
|
37
|
+
return left.length === right.length && left.every((message, index) => {
|
|
38
|
+
const other = right[index];
|
|
39
|
+
if (!other) return false;
|
|
40
|
+
return message.role === other.role
|
|
41
|
+
&& message.content === other.content
|
|
42
|
+
&& message.loading === other.loading;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function historyKeyFor(providerId: string, symbol: string): string {
|
|
47
|
+
return `conversation:${providerId}:${symbol}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export function __setAskAiHistoryForTests(symbol: string, messages: ChatMessage[]): void {
|
|
51
|
+
chatHistories.set(historyKeyFor("claude", symbol), messages);
|
|
37
52
|
chatHistories.set(symbol, messages);
|
|
38
53
|
}
|
|
39
54
|
|
|
@@ -58,7 +73,7 @@ export function AskAiDetailTab({ width, height, focused, onCapture }: DetailTabP
|
|
|
58
73
|
const [providers] = useState(() => detectProviders());
|
|
59
74
|
const defaultProviderId = resolveDefaultAiProviderId(providers);
|
|
60
75
|
const [providerId, setProviderId] = usePluginState<string>("providerId", defaultProviderId);
|
|
61
|
-
const [
|
|
76
|
+
const [conversationMessagesByKey, setConversationMessagesByKey] = useState<Record<string, ChatMessage[]>>({});
|
|
62
77
|
const [inputFocused, setInputFocused] = useState(false);
|
|
63
78
|
const [inputValue, setInputValue] = useState("");
|
|
64
79
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
@@ -73,6 +88,18 @@ export function AskAiDetailTab({ width, height, focused, onCapture }: DetailTabP
|
|
|
73
88
|
const conversationKey = tickerSymbol && currentProvider
|
|
74
89
|
? `conversation:${currentProvider.id}:${tickerSymbol}`
|
|
75
90
|
: "__conversation:none__";
|
|
91
|
+
const historyKey = tickerSymbol && currentProvider ? historyKeyFor(currentProvider.id, tickerSymbol) : null;
|
|
92
|
+
const messages = conversationMessagesByKey[conversationKey] ?? [];
|
|
93
|
+
const setMessages = useCallback((nextValue: SetStateAction<ChatMessage[]>) => {
|
|
94
|
+
setConversationMessagesByKey((previous) => {
|
|
95
|
+
const previousMessages = previous[conversationKey] ?? [];
|
|
96
|
+
const nextMessages = typeof nextValue === "function"
|
|
97
|
+
? (nextValue as (value: ChatMessage[]) => ChatMessage[])(previousMessages)
|
|
98
|
+
: nextValue;
|
|
99
|
+
if (sameMessages(previousMessages, nextMessages)) return previous;
|
|
100
|
+
return { ...previous, [conversationKey]: nextMessages };
|
|
101
|
+
});
|
|
102
|
+
}, [conversationKey]);
|
|
76
103
|
const [persistedConversation, setPersistedConversation] = usePluginState<PersistedConversation | null>(
|
|
77
104
|
conversationKey,
|
|
78
105
|
null,
|
|
@@ -81,36 +108,39 @@ export function AskAiDetailTab({ width, height, focused, onCapture }: DetailTabP
|
|
|
81
108
|
|
|
82
109
|
useEffect(() => {
|
|
83
110
|
if (!tickerSymbol) {
|
|
84
|
-
|
|
111
|
+
setConversationMessagesByKey((previous) => {
|
|
112
|
+
const previousMessages = previous[conversationKey] ?? [];
|
|
113
|
+
return previousMessages.length === 0 ? previous : { ...previous, [conversationKey]: [] };
|
|
114
|
+
});
|
|
85
115
|
return;
|
|
86
116
|
}
|
|
87
117
|
|
|
88
|
-
if (persistedConversation && Date.now() - persistedConversation.updatedAt <= ASK_AI_RETENTION_MS) {
|
|
89
|
-
chatHistories.set(
|
|
118
|
+
if (historyKey && persistedConversation && Date.now() - persistedConversation.updatedAt <= ASK_AI_RETENTION_MS) {
|
|
119
|
+
chatHistories.set(historyKey, persistedConversation.messages);
|
|
90
120
|
}
|
|
91
121
|
|
|
92
|
-
const nextMessages = chatHistories.get(tickerSymbol) ?? [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}, [persistedConversation, tickerSymbol]);
|
|
122
|
+
const nextMessages = (historyKey ? chatHistories.get(historyKey) : null) ?? chatHistories.get(tickerSymbol) ?? [];
|
|
123
|
+
setConversationMessagesByKey((previous) => {
|
|
124
|
+
const previousMessages = previous[conversationKey] ?? [];
|
|
125
|
+
return sameMessages(previousMessages, nextMessages) ? previous : { ...previous, [conversationKey]: nextMessages };
|
|
126
|
+
});
|
|
127
|
+
}, [conversationKey, historyKey, persistedConversation, tickerSymbol]);
|
|
99
128
|
|
|
100
129
|
useEffect(() => {
|
|
101
|
-
if (!tickerSymbol || !currentProvider || messages.length === 0) return;
|
|
130
|
+
if (!tickerSymbol || !currentProvider || !historyKey || messages.length === 0) return;
|
|
102
131
|
const hasLoading = messages.some((message) => message.loading);
|
|
103
132
|
if (hasLoading) {
|
|
104
|
-
chatHistories.set(
|
|
133
|
+
chatHistories.set(historyKey, messages);
|
|
105
134
|
return;
|
|
106
135
|
}
|
|
107
136
|
const trimmed = messages.slice(-ASK_AI_HISTORY_LIMIT);
|
|
108
|
-
chatHistories.set(
|
|
137
|
+
chatHistories.set(historyKey, trimmed);
|
|
138
|
+
if (persistedConversation && sameMessages(persistedConversation.messages, trimmed)) return;
|
|
109
139
|
setPersistedConversation({
|
|
110
140
|
updatedAt: Date.now(),
|
|
111
141
|
messages: trimmed,
|
|
112
142
|
});
|
|
113
|
-
}, [currentProvider, messages, setPersistedConversation, tickerSymbol]);
|
|
143
|
+
}, [currentProvider, historyKey, messages, persistedConversation, setPersistedConversation, tickerSymbol]);
|
|
114
144
|
|
|
115
145
|
useEffect(() => {
|
|
116
146
|
const sb = scrollRef.current;
|
|
@@ -65,23 +65,6 @@ export function resolveDefaultAiProviderId(providers = detectProviders()): strin
|
|
|
65
65
|
return getAvailableProviders(providers)[0]?.id ?? providers[0]?.id ?? "claude";
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function listAiProviderOptions(
|
|
69
|
-
preferredId?: string | null,
|
|
70
|
-
providers = detectProviders(),
|
|
71
|
-
): Array<{ label: string; value: string }> {
|
|
72
|
-
const ordered = [...providers].sort((left, right) => {
|
|
73
|
-
if (left.id === preferredId) return -1;
|
|
74
|
-
if (right.id === preferredId) return 1;
|
|
75
|
-
if (left.available !== right.available) return left.available ? -1 : 1;
|
|
76
|
-
return left.name.localeCompare(right.name);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return ordered.map((provider) => ({
|
|
80
|
-
value: provider.id,
|
|
81
|
-
label: provider.available ? provider.name : `${provider.name} (missing)`,
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
68
|
export function __setDetectedProvidersForTests(providers: AiProvider[] | null): void {
|
|
86
69
|
detectedProviders = providers;
|
|
87
70
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useReducer, useState } from "react";
|
|
3
|
-
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, testRender } from "../../../renderers/opentui/test-utils";
|
|
5
4
|
import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane-footer";
|
|
6
5
|
import { AppContext, PaneInstanceProvider, appReducer, createInitialState } from "../../../state/app-context";
|
|
7
6
|
import { createDefaultConfig } from "../../../types/config";
|
|
@@ -121,7 +120,7 @@ function ScreenerHarness({
|
|
|
121
120
|
|
|
122
121
|
return (
|
|
123
122
|
<AppContext value={{ state, dispatch }}>
|
|
124
|
-
<
|
|
123
|
+
<TestDialogProvider>
|
|
125
124
|
<PaneInstanceProvider paneId={PANE_ID}>
|
|
126
125
|
<PluginRenderProvider pluginId="ai" runtime={runtime}>
|
|
127
126
|
<PaneFooterProvider>
|
|
@@ -134,7 +133,7 @@ function ScreenerHarness({
|
|
|
134
133
|
</PaneFooterProvider>
|
|
135
134
|
</PluginRenderProvider>
|
|
136
135
|
</PaneInstanceProvider>
|
|
137
|
-
</
|
|
136
|
+
</TestDialogProvider>
|
|
138
137
|
</AppContext>
|
|
139
138
|
);
|
|
140
139
|
}
|
|
@@ -461,8 +461,6 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
461
461
|
const selectedResult = activeTab?.results.find((result) => result.symbol === cursorSymbol)
|
|
462
462
|
?? activeTab?.results[0]
|
|
463
463
|
?? null;
|
|
464
|
-
const selectedIdx = sortedTickers.findIndex((ticker) => ticker.metadata.ticker === cursorSymbol);
|
|
465
|
-
const safeSelectedIdx = selectedIdx >= 0 ? selectedIdx : 0;
|
|
466
464
|
const isRunningActiveTab = runState?.tabId === activeTab?.id;
|
|
467
465
|
const forceRunArmed = !!activeTab && forceConfirmTabId === activeTab.id;
|
|
468
466
|
|
|
@@ -753,7 +751,6 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
|
|
|
753
751
|
return;
|
|
754
752
|
}
|
|
755
753
|
|
|
756
|
-
const key = event.name;
|
|
757
754
|
if (event.name === "t") {
|
|
758
755
|
addTab();
|
|
759
756
|
return;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act } from "react";
|
|
2
|
+
import { act, useReducer } from "react";
|
|
3
3
|
import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
|
|
4
4
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
5
|
-
import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
|
|
5
|
+
import { AppContext, PaneInstanceProvider, appReducer, createInitialState } from "../../state/app-context";
|
|
6
6
|
import { createStatefulTestPluginRuntime } from "../../test-support/plugin-runtime";
|
|
7
7
|
import { createDefaultConfig } from "../../types/config";
|
|
8
8
|
import { Box } from "../../ui";
|
|
@@ -258,4 +258,91 @@ describe("AskAiTab", () => {
|
|
|
258
258
|
|
|
259
259
|
expect(opened).toEqual(["AAPL"]);
|
|
260
260
|
});
|
|
261
|
+
|
|
262
|
+
test("does not persist previous ticker messages while hydrating a newly selected ticker", async () => {
|
|
263
|
+
setProviders([
|
|
264
|
+
{ id: "claude", name: "Claude", command: "claude", available: true, buildArgs: () => [] },
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const runtime = createStatefulTestPluginRuntime();
|
|
268
|
+
runtime.setResumeState("ai", "conversation:claude:AAPL", {
|
|
269
|
+
updatedAt: Date.now(),
|
|
270
|
+
messages: [
|
|
271
|
+
{ role: "user", content: "AAPL question" },
|
|
272
|
+
{ role: "assistant", content: "AAPL answer", loading: false },
|
|
273
|
+
],
|
|
274
|
+
}, 1);
|
|
275
|
+
runtime.setResumeState("ai", "conversation:claude:MSFT", {
|
|
276
|
+
updatedAt: Date.now(),
|
|
277
|
+
messages: [
|
|
278
|
+
{ role: "user", content: "MSFT question" },
|
|
279
|
+
{ role: "assistant", content: "MSFT answer", loading: false },
|
|
280
|
+
],
|
|
281
|
+
}, 1);
|
|
282
|
+
|
|
283
|
+
let selectSymbol: ((symbol: string) => void) | null = null;
|
|
284
|
+
|
|
285
|
+
const config = createDefaultConfig("/tmp/gloomberb-ai-navigation");
|
|
286
|
+
const initialState = createInitialState(config);
|
|
287
|
+
initialState.focusedPaneId = PANE_ID;
|
|
288
|
+
initialState.paneState["portfolio-list:main"] = {
|
|
289
|
+
...initialState.paneState["portfolio-list:main"],
|
|
290
|
+
cursorSymbol: "AAPL",
|
|
291
|
+
};
|
|
292
|
+
initialState.tickers = new Map([
|
|
293
|
+
["AAPL", makeTicker("AAPL", "Apple Inc.")],
|
|
294
|
+
["MSFT", makeTicker("MSFT", "Microsoft Corporation")],
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
function NavigableAskAiHarness() {
|
|
298
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
299
|
+
selectSymbol = (symbol) => {
|
|
300
|
+
dispatch({
|
|
301
|
+
type: "UPDATE_PANE_STATE",
|
|
302
|
+
paneId: "portfolio-list:main",
|
|
303
|
+
patch: { cursorSymbol: symbol },
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<AppContext value={{ state, dispatch }}>
|
|
309
|
+
<PaneInstanceProvider paneId={PANE_ID}>
|
|
310
|
+
<PluginRenderProvider pluginId="ai" runtime={runtime}>
|
|
311
|
+
<PaneFooterProvider>
|
|
312
|
+
{(footer) => (
|
|
313
|
+
<Box flexDirection="column" width={60} height={12}>
|
|
314
|
+
<AskAiTab width={60} height={11} focused onCapture={() => {}} />
|
|
315
|
+
<PaneFooterBar footer={footer} focused width={60} />
|
|
316
|
+
</Box>
|
|
317
|
+
)}
|
|
318
|
+
</PaneFooterProvider>
|
|
319
|
+
</PluginRenderProvider>
|
|
320
|
+
</PaneInstanceProvider>
|
|
321
|
+
</AppContext>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await act(async () => {
|
|
326
|
+
testSetup = await testRender(<NavigableAskAiHarness />, {
|
|
327
|
+
width: 60,
|
|
328
|
+
height: 12,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
await flushFrame();
|
|
332
|
+
|
|
333
|
+
expect(testSetup.captureCharFrame()).toContain("AAPL answer");
|
|
334
|
+
|
|
335
|
+
await act(() => {
|
|
336
|
+
selectSymbol?.("MSFT");
|
|
337
|
+
});
|
|
338
|
+
await flushFrame();
|
|
339
|
+
|
|
340
|
+
const frame = testSetup.captureCharFrame();
|
|
341
|
+
expect(frame).toContain("MSFT answer");
|
|
342
|
+
expect(frame).not.toContain("AAPL answer");
|
|
343
|
+
expect((runtime.getResumeState("ai", "conversation:claude:MSFT") as any)?.messages).toEqual([
|
|
344
|
+
{ role: "user", content: "MSFT question" },
|
|
345
|
+
{ role: "assistant", content: "MSFT answer", loading: false },
|
|
346
|
+
]);
|
|
347
|
+
});
|
|
261
348
|
});
|
|
@@ -65,7 +65,7 @@ function Harness({
|
|
|
65
65
|
const runtime = createTestPluginRuntime({
|
|
66
66
|
getBrokerAdapter: (brokerType) => brokerType === "ibkr" ? ibkrBroker : null,
|
|
67
67
|
openCommandBar: (query) => calls.push(`command:${query ?? ""}`),
|
|
68
|
-
|
|
68
|
+
showPane: (paneId) => calls.push(`pane:${paneId}`),
|
|
69
69
|
connectBrokerInstance: async (instanceId) => { calls.push(`connect:${instanceId}`); },
|
|
70
70
|
syncBrokerInstance: async (instanceId) => { calls.push(`sync:${instanceId}`); },
|
|
71
71
|
updateBrokerInstance: async (instanceId) => { calls.push(`update:${instanceId}`); },
|
|
@@ -163,6 +163,6 @@ describe("BrokersPane", () => {
|
|
|
163
163
|
await pressKey("s");
|
|
164
164
|
await pressKey("o");
|
|
165
165
|
|
|
166
|
-
expect(calls).toEqual(["connect:ibkr-paper", "sync:ibkr-paper", "
|
|
166
|
+
expect(calls).toEqual(["connect:ibkr-paper", "sync:ibkr-paper", "pane:ibkr-trading"]);
|
|
167
167
|
});
|
|
168
168
|
});
|
|
@@ -2,6 +2,7 @@ import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
5
|
+
ConfirmDialog,
|
|
5
6
|
DataTableStackView,
|
|
6
7
|
NumberField,
|
|
7
8
|
SegmentedControl,
|
|
@@ -29,8 +30,7 @@ import type { BrokerAdapter, BrokerConfigField, BrokerProfileAction } from "../.
|
|
|
29
30
|
import type { BrokerInstanceConfig } from "../../../types/config";
|
|
30
31
|
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
31
32
|
import type { BrokerAccount } from "../../../types/trading";
|
|
32
|
-
import {
|
|
33
|
-
import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
|
|
33
|
+
import { useDialog, type PromptContext } from "../../../ui/dialog";
|
|
34
34
|
import { formatCurrency } from "../../../utils/format";
|
|
35
35
|
import { usePluginAppActions, usePluginBrokerActions } from "../../plugin-runtime";
|
|
36
36
|
import {
|
|
@@ -76,34 +76,6 @@ function isBrokerErrorMessage(message: string | null | undefined): boolean {
|
|
|
76
76
|
return normalized.includes("failed") || normalized.includes("required");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function ConfirmRemoveBrokerDialog({
|
|
80
|
-
resolve,
|
|
81
|
-
row,
|
|
82
|
-
}: PromptContext<boolean> & { row: BrokerProfileRow }) {
|
|
83
|
-
const confirm = useCallback(() => resolve(true), [resolve]);
|
|
84
|
-
const cancel = useCallback(() => resolve(false), [resolve]);
|
|
85
|
-
|
|
86
|
-
useDialogKeyboard((event) => {
|
|
87
|
-
event.stopPropagation();
|
|
88
|
-
if (event.name === "enter" || event.name === "return") confirm();
|
|
89
|
-
if (event.name === "escape") cancel();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<DialogFrame title="Disconnect broker?" footer="Enter disconnect · Esc cancel">
|
|
94
|
-
<Box flexDirection="column" width={58}>
|
|
95
|
-
<Text fg={colors.text}>{`Remove "${row.label}" and imported broker data?`}</Text>
|
|
96
|
-
<Text fg={colors.textDim}>Broker-managed portfolios, positions, and contracts will be removed.</Text>
|
|
97
|
-
<Box height={1} />
|
|
98
|
-
<Box flexDirection="row" gap={1}>
|
|
99
|
-
<Button label="Disconnect" variant="danger" onPress={confirm} />
|
|
100
|
-
<Button label="Back" variant="secondary" onPress={cancel} />
|
|
101
|
-
</Box>
|
|
102
|
-
</Box>
|
|
103
|
-
</DialogFrame>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
79
|
function brokerFieldLabel(field: BrokerConfigField, focused: boolean): string {
|
|
108
80
|
return focused ? `> ${field.label}` : ` ${field.label}`;
|
|
109
81
|
}
|
|
@@ -202,7 +174,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
202
174
|
const config = useAppSelector((state) => state.config);
|
|
203
175
|
const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
|
|
204
176
|
const dialog = useDialog();
|
|
205
|
-
const { openCommandBar,
|
|
177
|
+
const { openCommandBar, showPane } = usePluginAppActions();
|
|
206
178
|
const {
|
|
207
179
|
getBrokerAdapter,
|
|
208
180
|
connectBrokerInstance,
|
|
@@ -363,15 +335,26 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
363
335
|
setMessage(action.disabledReason ?? `${action.label} is unavailable for this profile.`);
|
|
364
336
|
return;
|
|
365
337
|
}
|
|
366
|
-
if (action.
|
|
367
|
-
}, [primaryProfileAction,
|
|
338
|
+
if (action.paneId) showPane(action.paneId);
|
|
339
|
+
}, [primaryProfileAction, showPane]);
|
|
368
340
|
|
|
369
341
|
const removeSelected = useCallback(async () => {
|
|
370
342
|
if (!selectedRow) return;
|
|
371
343
|
const confirmed = await dialog.prompt<boolean>({
|
|
372
344
|
closeOnClickOutside: true,
|
|
373
345
|
content: (ctx: PromptContext<boolean>) => (
|
|
374
|
-
<
|
|
346
|
+
<ConfirmDialog
|
|
347
|
+
{...ctx}
|
|
348
|
+
title="Disconnect broker?"
|
|
349
|
+
body={[
|
|
350
|
+
`Remove "${selectedRow.label}" and imported broker data?`,
|
|
351
|
+
"Broker-managed portfolios, positions, and contracts will be removed.",
|
|
352
|
+
]}
|
|
353
|
+
confirmLabel="Disconnect"
|
|
354
|
+
cancelLabel="Back"
|
|
355
|
+
width={58}
|
|
356
|
+
footer="Enter disconnect · Esc cancel"
|
|
357
|
+
/>
|
|
375
358
|
),
|
|
376
359
|
}).catch(() => false);
|
|
377
360
|
if (confirmed !== true) return;
|
|
@@ -392,7 +375,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
392
375
|
const hasSelectedRow = selectedRow !== null;
|
|
393
376
|
const selectedHasAdapter = !!selectedRow?.adapter;
|
|
394
377
|
const canUseSelectedBroker = selectedHasAdapter && !busy;
|
|
395
|
-
const canOpenSelectedAction = selectedProfileActions.some((action) => !action.disabled && action.
|
|
378
|
+
const canOpenSelectedAction = selectedProfileActions.some((action) => !action.disabled && action.paneId);
|
|
396
379
|
const canRemoveSelected = hasSelectedRow && !busy;
|
|
397
380
|
|
|
398
381
|
const footerActionsRef = useRef({
|
|
@@ -752,7 +735,7 @@ export const brokerManagerPlugin: GloomPlugin = {
|
|
|
752
735
|
keywords: ["broker", "brokers", "connection", "accounts", "sync"],
|
|
753
736
|
category: "navigation",
|
|
754
737
|
execute: () => {
|
|
755
|
-
ctx.
|
|
738
|
+
ctx.showPane("brokers");
|
|
756
739
|
},
|
|
757
740
|
});
|
|
758
741
|
},
|