gloomberb 0.7.1 → 0.7.3
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/README.md +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +47 -7
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +67 -11
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/ui/host.tsx +8 -2
- package/src/ui/index.tsx +1 -0
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
DEFAULT_TICKER_CHART_RESOLUTION,
|
|
9
9
|
normalizeChartResolution,
|
|
10
10
|
} from "../../../components/chart/chart-resolution";
|
|
11
|
+
import type { PriceSparklinePeriod } from "../../../components/price-sparkline-view";
|
|
11
12
|
import { getSharedRegistry } from "../../registry";
|
|
13
|
+
import { formatTickerListInput, MAX_TICKER_LIST_SIZE, parseTickerListInput } from "../../../utils/ticker-list";
|
|
12
14
|
|
|
13
15
|
type DetailTabSummary = { id: string; name: string; order: number };
|
|
14
16
|
|
|
@@ -24,6 +26,14 @@ export interface TickerDetailPaneSettings {
|
|
|
24
26
|
chartResolution: ReturnType<typeof normalizeChartResolution>;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export interface QuoteMonitorPaneSettings {
|
|
30
|
+
symbols: string[];
|
|
31
|
+
symbolsText: string;
|
|
32
|
+
chartPeriod: PriceSparklinePeriod;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_QUOTE_MONITOR_CHART_PERIOD: PriceSparklinePeriod = "1M";
|
|
36
|
+
|
|
27
37
|
export function getTickerDetailPaneSettings(
|
|
28
38
|
settings: Record<string, unknown> | undefined,
|
|
29
39
|
): TickerDetailPaneSettings {
|
|
@@ -100,15 +110,86 @@ export function buildTickerDetailSettingsDef(settings: TickerDetailPaneSettings)
|
|
|
100
110
|
};
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
function coerceQuoteMonitorSymbols(value: unknown): string[] {
|
|
114
|
+
if (!Array.isArray(value)) return [];
|
|
115
|
+
const parsed = value.filter((entry): entry is string => typeof entry === "string");
|
|
116
|
+
try {
|
|
117
|
+
return parseTickerListInput(parsed.join(", "), MAX_TICKER_LIST_SIZE);
|
|
118
|
+
} catch {
|
|
119
|
+
return parsed.slice(0, MAX_TICKER_LIST_SIZE);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildQuoteMonitorPaneTitle(symbols: string[]): string {
|
|
124
|
+
if (symbols.length === 0) return "Quote Monitor";
|
|
125
|
+
if (symbols.length <= 3) return symbols.join(" · ");
|
|
126
|
+
return `${symbols.slice(0, 2).join(" · ")} +${symbols.length - 2}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getQuoteMonitorPaneSettings(
|
|
130
|
+
settings: Record<string, unknown> | undefined,
|
|
131
|
+
fallbackSymbol?: string | null,
|
|
132
|
+
): QuoteMonitorPaneSettings {
|
|
133
|
+
const storedSymbols = coerceQuoteMonitorSymbols(settings?.symbols);
|
|
134
|
+
const legacySymbol = typeof settings?.symbol === "string" ? settings.symbol.trim() : "";
|
|
135
|
+
const storedText = typeof settings?.symbolsText === "string" ? settings.symbolsText : "";
|
|
136
|
+
let symbols = storedSymbols;
|
|
137
|
+
|
|
138
|
+
if (symbols.length === 0 && storedText.trim().length > 0) {
|
|
139
|
+
try {
|
|
140
|
+
symbols = parseTickerListInput(storedText, MAX_TICKER_LIST_SIZE);
|
|
141
|
+
} catch {
|
|
142
|
+
symbols = [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (symbols.length === 0 && legacySymbol) {
|
|
146
|
+
try {
|
|
147
|
+
symbols = parseTickerListInput(legacySymbol, MAX_TICKER_LIST_SIZE);
|
|
148
|
+
} catch {
|
|
149
|
+
symbols = [legacySymbol.toUpperCase()];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (symbols.length === 0 && fallbackSymbol) {
|
|
153
|
+
symbols = [fallbackSymbol.toUpperCase()];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
symbols,
|
|
158
|
+
symbolsText: storedText.trim().length > 0 ? storedText : formatTickerListInput(symbols),
|
|
159
|
+
chartPeriod: settings?.chartPeriod === "1D"
|
|
160
|
+
|| settings?.chartPeriod === "1W"
|
|
161
|
+
|| settings?.chartPeriod === "1M"
|
|
162
|
+
|| settings?.chartPeriod === "1Y"
|
|
163
|
+
? settings.chartPeriod
|
|
164
|
+
: DEFAULT_QUOTE_MONITOR_CHART_PERIOD,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
103
168
|
export function buildQuoteMonitorSettingsDef(): PaneSettingsDef {
|
|
104
169
|
return {
|
|
105
170
|
title: "Quote Monitor Settings",
|
|
171
|
+
values: {
|
|
172
|
+
chartPeriod: DEFAULT_QUOTE_MONITOR_CHART_PERIOD,
|
|
173
|
+
},
|
|
106
174
|
fields: [
|
|
107
175
|
{
|
|
108
|
-
key: "
|
|
109
|
-
label: "
|
|
176
|
+
key: "symbolsText",
|
|
177
|
+
label: "Tickers",
|
|
178
|
+
description: `Enter up to ${MAX_TICKER_LIST_SIZE} tickers separated by commas.`,
|
|
110
179
|
type: "text",
|
|
111
|
-
placeholder: "AAPL",
|
|
180
|
+
placeholder: "AAPL, MSFT, NVDA",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "chartPeriod",
|
|
184
|
+
label: "Chart Period",
|
|
185
|
+
description: "Set the period used for quote sparklines and range labels.",
|
|
186
|
+
type: "select",
|
|
187
|
+
options: [
|
|
188
|
+
{ value: "1D", label: "1D" },
|
|
189
|
+
{ value: "1W", label: "1W" },
|
|
190
|
+
{ value: "1M", label: "1M" },
|
|
191
|
+
{ value: "1Y", label: "1Y" },
|
|
192
|
+
],
|
|
112
193
|
},
|
|
113
194
|
],
|
|
114
195
|
};
|
|
@@ -820,7 +820,7 @@ describe("TickerDetailPane", () => {
|
|
|
820
820
|
expect(frame).toContain("+25.00%");
|
|
821
821
|
});
|
|
822
822
|
|
|
823
|
-
test("
|
|
823
|
+
test("renders an Overview fallback without overwriting a temporarily unavailable active tab", async () => {
|
|
824
824
|
const gatewayConfig = createDetailConfig("AAPL", [createGatewayInstance()]);
|
|
825
825
|
const noGatewayConfig = createDetailConfig("AAPL");
|
|
826
826
|
setSharedRegistryForTests(makeRegistry());
|
|
@@ -844,7 +844,7 @@ describe("TickerDetailPane", () => {
|
|
|
844
844
|
await flushFrame();
|
|
845
845
|
|
|
846
846
|
const frame = testSetup.captureCharFrame();
|
|
847
|
-
expect(frame).toContain("active:
|
|
847
|
+
expect(frame).toContain("active:ibkr-trade");
|
|
848
848
|
expect(frame).not.toContain("Trade");
|
|
849
849
|
});
|
|
850
850
|
|
|
@@ -16,6 +16,7 @@ import { colors, priceColor } from "../../theme/colors";
|
|
|
16
16
|
import type { PaneProps } from "../../types/plugin";
|
|
17
17
|
import type { Quote } from "../../types/financials";
|
|
18
18
|
import { formatCurrency, padTo } from "../../utils/format";
|
|
19
|
+
import { isPlainKey } from "../../utils/keyboard";
|
|
19
20
|
import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
|
|
20
21
|
import { getBrokerInstance } from "../../utils/broker-instances";
|
|
21
22
|
import { usePluginPaneActions } from "../plugin-runtime";
|
|
@@ -295,13 +296,13 @@ export function TradingPane({ focused, width, height }: PaneProps) {
|
|
|
295
296
|
if (!focused) return;
|
|
296
297
|
event.stopPropagation?.();
|
|
297
298
|
|
|
298
|
-
if (event
|
|
299
|
+
if (isPlainKey(event, "j", "down")) {
|
|
299
300
|
const nextIndex = Math.min(tradeState.selectedOpenOrderIndex + 1, Math.max(0, gatewaySnapshot.openOrders.length - 1));
|
|
300
301
|
updateTradingPaneState({ selectedOpenOrderIndex: nextIndex });
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
if (event
|
|
305
|
+
if (isPlainKey(event, "k", "up")) {
|
|
305
306
|
const nextIndex = Math.max(0, tradeState.selectedOpenOrderIndex - 1);
|
|
306
307
|
updateTradingPaneState({ selectedOpenOrderIndex: nextIndex });
|
|
307
308
|
return;
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import type { BrokerAdapter } from "../types/broker";
|
|
21
21
|
import type { PluginCapability } from "../capabilities";
|
|
22
22
|
import type { DataProvider } from "../types/data-provider";
|
|
23
|
-
import type { AppNotificationRequest, BrokerInstanceUpdateOptions } from "../types/plugin";
|
|
23
|
+
import type { AppNotificationRequest, BrokerInstanceUpdateOptions, PinTickerOptions } from "../types/plugin";
|
|
24
24
|
|
|
25
25
|
export interface PluginRuntimeAccess {
|
|
26
26
|
getMarketData(): DataProvider | null;
|
|
@@ -30,7 +30,7 @@ export interface PluginRuntimeAccess {
|
|
|
30
30
|
updateBrokerInstance(instanceId: string, values: Record<string, unknown>, options?: BrokerInstanceUpdateOptions): Promise<void>;
|
|
31
31
|
syncBrokerInstance(instanceId: string): Promise<void>;
|
|
32
32
|
removeBrokerInstance(instanceId: string): Promise<void>;
|
|
33
|
-
pinTicker(symbol: string, options?:
|
|
33
|
+
pinTicker(symbol: string, options?: PinTickerOptions): void;
|
|
34
34
|
navigateTicker(symbol: string, options?: { sourcePaneId?: string | null }): void;
|
|
35
35
|
selectTicker(symbol: string, paneId?: string): void;
|
|
36
36
|
switchTab(tabId: string, paneId?: string): void;
|
|
@@ -222,6 +222,61 @@ describe("PluginRegistry detail tabs", () => {
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
describe("PluginRegistry pane settings", () => {
|
|
225
|
+
test("resolves effective pane setting values from settings definitions", async () => {
|
|
226
|
+
const registry = createRegistry();
|
|
227
|
+
const config = createDefaultConfig("/tmp/gloomberb-pane-settings-test");
|
|
228
|
+
config.layout = {
|
|
229
|
+
dockRoot: { kind: "pane", instanceId: "test-pane:main" },
|
|
230
|
+
instances: [{
|
|
231
|
+
instanceId: "test-pane:main",
|
|
232
|
+
paneId: "test-pane",
|
|
233
|
+
binding: { kind: "none" },
|
|
234
|
+
settings: {},
|
|
235
|
+
}],
|
|
236
|
+
floating: [],
|
|
237
|
+
};
|
|
238
|
+
registry.getConfigFn = () => config;
|
|
239
|
+
registry.getLayoutFn = () => config.layout;
|
|
240
|
+
|
|
241
|
+
await registry.register(plugin("tables", (ctx) => {
|
|
242
|
+
ctx.registerPane({
|
|
243
|
+
id: "test-pane",
|
|
244
|
+
name: "Test Pane",
|
|
245
|
+
defaultPosition: "right",
|
|
246
|
+
component: () => null,
|
|
247
|
+
settings: {
|
|
248
|
+
values: {
|
|
249
|
+
columnIds: ["ticker", "price"],
|
|
250
|
+
collectionScope: "all",
|
|
251
|
+
},
|
|
252
|
+
fields: [
|
|
253
|
+
{
|
|
254
|
+
key: "columnIds",
|
|
255
|
+
label: "Columns",
|
|
256
|
+
type: "ordered-multi-select",
|
|
257
|
+
options: [
|
|
258
|
+
{ value: "ticker", label: "Ticker" },
|
|
259
|
+
{ value: "price", label: "Price" },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: "collectionScope",
|
|
264
|
+
label: "Collections",
|
|
265
|
+
type: "select",
|
|
266
|
+
options: [{ value: "all", label: "All" }],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const descriptor = registry.resolvePaneSettings("test-pane:main");
|
|
274
|
+
|
|
275
|
+
expect(descriptor?.context.settings.columnIds).toEqual(["ticker", "price"]);
|
|
276
|
+
expect(descriptor?.context.settings.collectionScope).toBe("all");
|
|
277
|
+
expect(descriptor?.rawSettings.columnIds).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
|
|
225
280
|
test("resolves plugin-scoped pane setting values from plugin config", async () => {
|
|
226
281
|
const registry = createRegistry();
|
|
227
282
|
const config = createDefaultConfig("/tmp/gloomberb-pane-settings-test");
|
package/src/plugins/registry.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
PaneSettingsDef,
|
|
27
27
|
PaneTemplateCreateOptions,
|
|
28
28
|
PaneTemplateDef,
|
|
29
|
+
PinTickerOptions,
|
|
29
30
|
PluginPaneSettingsState,
|
|
30
31
|
PluginResumeState,
|
|
31
32
|
TickerAction,
|
|
@@ -140,7 +141,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
140
141
|
createPaneFromTemplateAsyncFn: ((templateId: string, options?: PaneTemplateCreateOptions) => Promise<void>) = async () => {};
|
|
141
142
|
hidePaneFn: ((paneId: string) => void) = () => {};
|
|
142
143
|
focusPaneFn: ((paneId: string) => void) = () => {};
|
|
143
|
-
pinTickerFn: ((symbol: string, options?:
|
|
144
|
+
pinTickerFn: ((symbol: string, options?: PinTickerOptions) => void) = () => {};
|
|
144
145
|
navigateTickerFn: ((symbol: string, options?: { sourcePaneId?: string | null }) => void) = () => {};
|
|
145
146
|
getMarketData = () => this.marketData;
|
|
146
147
|
getCapability = (capabilityId: string) => this.capabilities.get(capabilityId)?.capability ?? null;
|
|
@@ -151,7 +152,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
151
152
|
);
|
|
152
153
|
syncBrokerInstance = (instanceId: string) => this.syncBrokerInstanceFn(instanceId);
|
|
153
154
|
removeBrokerInstance = (instanceId: string) => this.removeBrokerInstanceFn(instanceId);
|
|
154
|
-
pinTicker = (symbol: string, options?:
|
|
155
|
+
pinTicker = (symbol: string, options?: PinTickerOptions) => {
|
|
155
156
|
this.pinTickerFn(symbol, options);
|
|
156
157
|
};
|
|
157
158
|
navigateTicker = (symbol: string, options?: { sourcePaneId?: string | null }) => {
|
|
@@ -444,6 +445,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
444
445
|
pane: PaneInstanceConfig;
|
|
445
446
|
paneDef: PaneDef;
|
|
446
447
|
settingsDef: PaneSettingsDef;
|
|
448
|
+
rawSettings: Record<string, unknown>;
|
|
447
449
|
context: PaneSettingsContext;
|
|
448
450
|
} | null {
|
|
449
451
|
const targetPaneId = this.resolvePaneTarget(paneId);
|
|
@@ -483,8 +485,12 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
483
485
|
: paneDef.settings;
|
|
484
486
|
if (!settingsDef) return null;
|
|
485
487
|
|
|
488
|
+
const rawSettings = { ...paneSettings };
|
|
489
|
+
const resolvedSettings = {
|
|
490
|
+
...paneSettings,
|
|
491
|
+
...(settingsDef.values ?? {}),
|
|
492
|
+
};
|
|
486
493
|
if (pluginId) {
|
|
487
|
-
const resolvedSettings = { ...paneSettings };
|
|
488
494
|
for (const field of settingsDef.fields) {
|
|
489
495
|
if (field.storage !== "plugin") continue;
|
|
490
496
|
const configValue = this.getConfigState(pluginId, field.key);
|
|
@@ -494,8 +500,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
494
500
|
resolvedSettings[field.key] = configValue;
|
|
495
501
|
}
|
|
496
502
|
}
|
|
497
|
-
context.settings = resolvedSettings;
|
|
498
503
|
}
|
|
504
|
+
context.settings = resolvedSettings;
|
|
499
505
|
|
|
500
506
|
return {
|
|
501
507
|
paneId: targetPaneId,
|
|
@@ -503,6 +509,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
503
509
|
pane,
|
|
504
510
|
paneDef,
|
|
505
511
|
settingsDef,
|
|
512
|
+
rawSettings,
|
|
506
513
|
context,
|
|
507
514
|
};
|
|
508
515
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { createPaneInstance, type LayoutConfig, type PaneInstanceConfig } from "../types/config";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
findFixedTickerPaneForSymbol,
|
|
5
|
+
resolveTickerNavigationReplacementPane,
|
|
5
6
|
shouldFocusTickerNavigationTarget,
|
|
6
7
|
} from "./ticker-navigation";
|
|
7
8
|
|
|
@@ -20,8 +21,8 @@ function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
describe("
|
|
24
|
-
test("
|
|
24
|
+
describe("resolveTickerNavigationReplacementPane", () => {
|
|
25
|
+
test("only replaces the ticker detail pane that opened ticker navigation", () => {
|
|
25
26
|
const layout = createLayout([
|
|
26
27
|
createPaneInstance("portfolio-list", {
|
|
27
28
|
instanceId: "portfolio-list:main",
|
|
@@ -42,8 +43,37 @@ describe("resolveTickerNavigationDetailPane", () => {
|
|
|
42
43
|
]);
|
|
43
44
|
|
|
44
45
|
expect(
|
|
45
|
-
|
|
46
|
-
).
|
|
46
|
+
resolveTickerNavigationReplacementPane(layout, "comparison-chart:main")?.instanceId,
|
|
47
|
+
).toBeUndefined();
|
|
48
|
+
expect(
|
|
49
|
+
resolveTickerNavigationReplacementPane(layout, "ticker-detail:portfolio")?.instanceId,
|
|
50
|
+
).toBe("ticker-detail:portfolio");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("findFixedTickerPaneForSymbol", () => {
|
|
55
|
+
test("finds only a visible fixed pane for the requested symbol", () => {
|
|
56
|
+
const layout = createLayout([
|
|
57
|
+
createPaneInstance("portfolio-list", {
|
|
58
|
+
instanceId: "portfolio-list:main",
|
|
59
|
+
binding: { kind: "none" },
|
|
60
|
+
}),
|
|
61
|
+
createPaneInstance("ticker-detail", {
|
|
62
|
+
instanceId: "ticker-detail:follow",
|
|
63
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
64
|
+
}),
|
|
65
|
+
createPaneInstance("ticker-detail", {
|
|
66
|
+
instanceId: "ticker-detail:aapl",
|
|
67
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
68
|
+
}),
|
|
69
|
+
createPaneInstance("ticker-detail", {
|
|
70
|
+
instanceId: "ticker-detail:msft",
|
|
71
|
+
binding: { kind: "fixed", symbol: "MSFT" },
|
|
72
|
+
}),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
expect(findFixedTickerPaneForSymbol(layout, "ticker-detail", "AAPL")?.instanceId).toBe("ticker-detail:aapl");
|
|
76
|
+
expect(findFixedTickerPaneForSymbol(layout, "ticker-detail", "NVDA")).toBeNull();
|
|
47
77
|
});
|
|
48
78
|
});
|
|
49
79
|
|
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
import { findPaneInstance, type LayoutConfig, type PaneInstanceConfig } from "../types/config";
|
|
2
2
|
import { isPaneInLayout } from "./pane-manager";
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function resolveTickerNavigationReplacementPane(
|
|
5
5
|
layout: LayoutConfig,
|
|
6
6
|
sourcePaneId: string | null,
|
|
7
7
|
): PaneInstanceConfig | null {
|
|
8
8
|
const sourceInstance = sourcePaneId ? findPaneInstance(layout, sourcePaneId) : null;
|
|
9
|
+
return sourceInstance?.paneId === "ticker-detail" && isPaneInLayout(layout, sourceInstance.instanceId)
|
|
10
|
+
? sourceInstance
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
?? layout.instances.find((instance) =>
|
|
22
|
-
instance.paneId === "ticker-detail"
|
|
23
|
-
&& instance.binding?.kind === "follow"
|
|
24
|
-
&& isPaneInLayout(layout, instance.instanceId)
|
|
25
|
-
)
|
|
26
|
-
?? layout.instances.find((instance) =>
|
|
27
|
-
instance.paneId === "ticker-detail"
|
|
28
|
-
&& isPaneInLayout(layout, instance.instanceId)
|
|
29
|
-
)
|
|
30
|
-
?? null;
|
|
14
|
+
export function findFixedTickerPaneForSymbol(
|
|
15
|
+
layout: LayoutConfig,
|
|
16
|
+
paneId: string,
|
|
17
|
+
symbol: string,
|
|
18
|
+
): PaneInstanceConfig | null {
|
|
19
|
+
return layout.instances.find((instance) =>
|
|
20
|
+
instance.paneId === paneId
|
|
21
|
+
&& instance.binding?.kind === "fixed"
|
|
22
|
+
&& instance.binding.symbol === symbol
|
|
23
|
+
&& isPaneInLayout(layout, instance.instanceId)
|
|
24
|
+
) ?? null;
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
export function shouldFocusTickerNavigationTarget({
|
|
@@ -21,6 +21,15 @@ describe("applicationMenuCommand", () => {
|
|
|
21
21
|
})).toBeNull();
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
test("parses the native devtools command", () => {
|
|
25
|
+
expect(applicationMenuCommand({
|
|
26
|
+
data: {
|
|
27
|
+
action: ELECTROBUN_APPLICATION_MENU_ACTION,
|
|
28
|
+
data: { type: "open-devtools" },
|
|
29
|
+
},
|
|
30
|
+
})).toEqual({ type: "open-devtools" });
|
|
31
|
+
});
|
|
32
|
+
|
|
24
33
|
test("ignores unknown command types", () => {
|
|
25
34
|
expect(applicationMenuCommand({
|
|
26
35
|
data: {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ELECTROBUN_APPLICATION_MENU_ACTION,
|
|
3
|
+
type ElectrobunApplicationMenuCommand,
|
|
4
|
+
} from "./application-menu";
|
|
3
5
|
|
|
4
6
|
function record(value: unknown): Record<string, unknown> | null {
|
|
5
7
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -19,7 +21,7 @@ function applicationMenuClickPayload(event: unknown): Record<string, unknown> |
|
|
|
19
21
|
return typeof eventRecord.action === "string" ? eventRecord : null;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
function normalizeApplicationMenuCommand(value: unknown):
|
|
24
|
+
function normalizeApplicationMenuCommand(value: unknown): ElectrobunApplicationMenuCommand | null {
|
|
23
25
|
const command = record(value);
|
|
24
26
|
if (!command || typeof command.type !== "string") return null;
|
|
25
27
|
|
|
@@ -42,13 +44,14 @@ function normalizeApplicationMenuCommand(value: unknown): DesktopApplicationMenu
|
|
|
42
44
|
case "layout-undo":
|
|
43
45
|
case "layout-redo":
|
|
44
46
|
case "layout-gridlock":
|
|
47
|
+
case "open-devtools":
|
|
45
48
|
return { type: command.type };
|
|
46
49
|
default:
|
|
47
50
|
return null;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
export function applicationMenuCommand(event: unknown):
|
|
54
|
+
export function applicationMenuCommand(event: unknown): ElectrobunApplicationMenuCommand | null {
|
|
52
55
|
const payload = applicationMenuClickPayload(event);
|
|
53
56
|
if (payload?.action !== ELECTROBUN_APPLICATION_MENU_ACTION) return null;
|
|
54
57
|
return normalizeApplicationMenuCommand(payload.data);
|
|
@@ -4,9 +4,13 @@ import type { DesktopApplicationMenuCommand } from "../../../types/desktop-menu"
|
|
|
4
4
|
export const ELECTROBUN_APPLICATION_MENU_ACTION = "gloom.application-menu.select";
|
|
5
5
|
export const GITHUB_ISSUE_URL = "https://github.com/vincelwt/gloomberb/issues/new/choose";
|
|
6
6
|
|
|
7
|
+
export type ElectrobunApplicationMenuCommand =
|
|
8
|
+
| DesktopApplicationMenuCommand
|
|
9
|
+
| { type: "open-devtools" };
|
|
10
|
+
|
|
7
11
|
function commandItem(
|
|
8
12
|
label: string,
|
|
9
|
-
command:
|
|
13
|
+
command: ElectrobunApplicationMenuCommand,
|
|
10
14
|
options: { accelerator?: string } = {},
|
|
11
15
|
): ApplicationMenuItemConfig {
|
|
12
16
|
return {
|
|
@@ -72,6 +76,7 @@ export function buildApplicationMenu(): ApplicationMenuItemConfig[] {
|
|
|
72
76
|
label: "View",
|
|
73
77
|
submenu: [
|
|
74
78
|
openCommandBar("Open Command Bar", "", { accelerator: "CmdOrCtrl+K" }),
|
|
79
|
+
commandItem("Open Developer Tools", { type: "open-devtools" }),
|
|
75
80
|
{ type: "divider" },
|
|
76
81
|
commandItem("Toggle Status Bar", { type: "toggle-status-bar" }),
|
|
77
82
|
{ type: "divider" },
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { AppSessionSnapshot } from "../../../core/state/session-persistence";
|
|
2
|
-
import type
|
|
2
|
+
import { clonePaneStateMap, syncConfigActiveLayoutState, type PaneRuntimeState } from "../../../core/state/app-state";
|
|
3
3
|
import { cloneLayout, type AppConfig } from "../../../types/config";
|
|
4
4
|
import type { DesktopSharedStateSnapshot } from "../../../types/desktop-window";
|
|
5
5
|
import { detachPaneToFrame, dockPane, insertAtRootEdge, removePane } from "../../../plugins/pane-manager";
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
function cloneSavedLayouts(config: AppConfig): AppConfig["layouts"] {
|
|
8
|
+
return config.layouts.map((entry) => ({
|
|
9
|
+
...entry,
|
|
10
|
+
layout: cloneLayout(entry.layout),
|
|
11
|
+
paneState: entry.paneState ? clonePaneStateMap(entry.paneState) : entry.paneState,
|
|
12
|
+
}));
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
function filterPaneState(
|
|
@@ -20,17 +22,6 @@ function filterPaneState(
|
|
|
20
22
|
);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
function syncActiveLayout(config: AppConfig): AppConfig {
|
|
24
|
-
return {
|
|
25
|
-
...config,
|
|
26
|
-
layouts: config.layouts.map((entry, index) => (
|
|
27
|
-
index === config.activeLayoutIndex
|
|
28
|
-
? { ...entry, layout: cloneLayout(config.layout) }
|
|
29
|
-
: entry
|
|
30
|
-
)),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
export interface DesktopWorkspace {
|
|
35
26
|
getSnapshot(): DesktopSharedStateSnapshot;
|
|
36
27
|
syncMainState(snapshot: DesktopSharedStateSnapshot): DesktopSharedStateSnapshot;
|
|
@@ -55,22 +46,37 @@ export function createDesktopWorkspace(
|
|
|
55
46
|
config: AppConfig,
|
|
56
47
|
sessionSnapshot: AppSessionSnapshot | null,
|
|
57
48
|
): DesktopWorkspace {
|
|
49
|
+
const savedPaneState = config.layouts[config.activeLayoutIndex]?.paneState ?? {};
|
|
50
|
+
const initialPaneState = filterPaneState(config.layout, clonePaneStateMap({
|
|
51
|
+
...(sessionSnapshot?.paneState ?? {}),
|
|
52
|
+
...savedPaneState,
|
|
53
|
+
}));
|
|
54
|
+
const initialFocusedPaneId = config.layouts[config.activeLayoutIndex]?.focusedPaneId
|
|
55
|
+
?? sessionSnapshot?.focusedPaneId
|
|
56
|
+
?? null;
|
|
57
|
+
const initialActivePanel = config.layouts[config.activeLayoutIndex]?.activePanel
|
|
58
|
+
?? (sessionSnapshot?.activePanel === "right" ? "right" : "left");
|
|
58
59
|
let sharedState: DesktopSharedStateSnapshot = {
|
|
59
|
-
config:
|
|
60
|
-
paneState:
|
|
61
|
-
focusedPaneId:
|
|
62
|
-
activePanel:
|
|
60
|
+
config: syncConfigActiveLayoutState(config, initialPaneState, initialFocusedPaneId, initialActivePanel),
|
|
61
|
+
paneState: initialPaneState,
|
|
62
|
+
focusedPaneId: initialFocusedPaneId,
|
|
63
|
+
activePanel: initialActivePanel,
|
|
63
64
|
statusBarVisible: sessionSnapshot?.statusBarVisible !== false,
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
const updateConfig = (nextConfig: AppConfig, options?: { layoutChanged?: boolean }) => {
|
|
67
|
-
const syncedConfig =
|
|
68
|
+
const syncedConfig = syncConfigActiveLayoutState(
|
|
69
|
+
nextConfig,
|
|
70
|
+
sharedState.paneState,
|
|
71
|
+
sharedState.focusedPaneId,
|
|
72
|
+
sharedState.activePanel,
|
|
73
|
+
);
|
|
68
74
|
sharedState = {
|
|
69
75
|
...sharedState,
|
|
70
76
|
config: {
|
|
71
77
|
...syncedConfig,
|
|
72
78
|
layout: cloneLayout(syncedConfig.layout),
|
|
73
|
-
layouts: syncedConfig
|
|
79
|
+
layouts: cloneSavedLayouts(syncedConfig),
|
|
74
80
|
},
|
|
75
81
|
paneState: filterPaneState(syncedConfig.layout, sharedState.paneState),
|
|
76
82
|
layoutChanged: options?.layoutChanged,
|
|
@@ -82,7 +88,7 @@ export function createDesktopWorkspace(
|
|
|
82
88
|
config: {
|
|
83
89
|
...sharedState.config,
|
|
84
90
|
layout: cloneLayout(sharedState.config.layout),
|
|
85
|
-
layouts: sharedState.config
|
|
91
|
+
layouts: cloneSavedLayouts(sharedState.config),
|
|
86
92
|
},
|
|
87
93
|
paneState: clonePaneStateMap(sharedState.paneState),
|
|
88
94
|
focusedPaneId: sharedState.focusedPaneId,
|
|
@@ -94,12 +100,17 @@ export function createDesktopWorkspace(
|
|
|
94
100
|
return {
|
|
95
101
|
getSnapshot,
|
|
96
102
|
syncMainState(snapshot) {
|
|
97
|
-
const syncedConfig =
|
|
103
|
+
const syncedConfig = syncConfigActiveLayoutState(
|
|
104
|
+
snapshot.config,
|
|
105
|
+
snapshot.paneState,
|
|
106
|
+
snapshot.focusedPaneId,
|
|
107
|
+
snapshot.activePanel,
|
|
108
|
+
);
|
|
98
109
|
sharedState = {
|
|
99
110
|
config: {
|
|
100
111
|
...syncedConfig,
|
|
101
112
|
layout: cloneLayout(syncedConfig.layout),
|
|
102
|
-
layouts: syncedConfig
|
|
113
|
+
layouts: cloneSavedLayouts(syncedConfig),
|
|
103
114
|
},
|
|
104
115
|
paneState: filterPaneState(syncedConfig.layout, clonePaneStateMap(snapshot.paneState)),
|
|
105
116
|
focusedPaneId: snapshot.focusedPaneId,
|
|
@@ -113,12 +124,19 @@ export function createDesktopWorkspace(
|
|
|
113
124
|
return updateConfig(config, options);
|
|
114
125
|
},
|
|
115
126
|
replaceDetachedPaneState(paneId, paneState) {
|
|
127
|
+
const nextPaneState = filterPaneState(sharedState.config.layout, {
|
|
128
|
+
...sharedState.paneState,
|
|
129
|
+
[paneId]: { ...paneState },
|
|
130
|
+
});
|
|
116
131
|
sharedState = {
|
|
117
132
|
...sharedState,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
config: syncConfigActiveLayoutState(
|
|
134
|
+
sharedState.config,
|
|
135
|
+
nextPaneState,
|
|
136
|
+
sharedState.focusedPaneId,
|
|
137
|
+
sharedState.activePanel,
|
|
138
|
+
),
|
|
139
|
+
paneState: nextPaneState,
|
|
122
140
|
};
|
|
123
141
|
return getSnapshot();
|
|
124
142
|
},
|