gloomberb 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- 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 +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- 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/core/state/session-persistence.ts +25 -0
- 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-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- 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/screener-pane.tsx +3 -0
- 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 +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- 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/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +1 -3
- 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 +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- 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 +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- 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 +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- 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 +6 -4
- 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/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -183,31 +183,70 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
|
183
183
|
fetchGenRef.current += 1;
|
|
184
184
|
const gen = fetchGenRef.current;
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
setQuotes((prev) => {
|
|
187
|
+
const next = new Map(prev);
|
|
188
|
+
for (const entry of WORLD_INDICES) {
|
|
189
189
|
const existing = next.get(entry.symbol);
|
|
190
190
|
next.set(entry.symbol, { quote: existing?.quote ?? null, loading: true, error: null });
|
|
191
|
+
}
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const loadQuotes = async (): Promise<QuoteMap> => {
|
|
196
|
+
const next = new Map<string, IndexQuoteState>();
|
|
197
|
+
if (dataProvider.getQuotesBatch) {
|
|
198
|
+
const results = await dataProvider.getQuotesBatch(
|
|
199
|
+
WORLD_INDICES.map((entry) => ({ symbol: entry.symbol, exchange: "" })),
|
|
200
|
+
);
|
|
201
|
+
const bySymbol = new Map(results.map((result) => [result.target.symbol, result]));
|
|
202
|
+
for (const entry of WORLD_INDICES) {
|
|
203
|
+
const result = bySymbol.get(entry.symbol);
|
|
204
|
+
next.set(entry.symbol, {
|
|
205
|
+
quote: result?.quote ?? null,
|
|
206
|
+
loading: false,
|
|
207
|
+
error: result?.error
|
|
208
|
+
? result.error instanceof Error ? result.error.message : String(result.error)
|
|
209
|
+
: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
191
212
|
return next;
|
|
192
|
-
}
|
|
213
|
+
}
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const next = new Map(prev);
|
|
215
|
+
await Promise.all(WORLD_INDICES.map(async (entry) => {
|
|
216
|
+
try {
|
|
217
|
+
const quote = await dataProvider.getQuote(entry.symbol, "");
|
|
198
218
|
next.set(entry.symbol, { quote, loading: false, error: null });
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
} catch (err: unknown) {
|
|
220
|
+
next.set(entry.symbol, {
|
|
221
|
+
quote: null,
|
|
222
|
+
loading: false,
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
return next;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
loadQuotes().then((nextQuotes) => {
|
|
231
|
+
if (fetchGenRef.current !== gen) return;
|
|
232
|
+
setQuotes((prev) => {
|
|
233
|
+
const next = new Map(prev);
|
|
234
|
+
for (const [symbol, state] of nextQuotes) {
|
|
235
|
+
next.set(symbol, state);
|
|
236
|
+
}
|
|
237
|
+
return next;
|
|
209
238
|
});
|
|
210
|
-
}
|
|
239
|
+
}).catch((err: unknown) => {
|
|
240
|
+
if (fetchGenRef.current !== gen) return;
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
setQuotes((prev) => {
|
|
243
|
+
const next = new Map(prev);
|
|
244
|
+
for (const entry of WORLD_INDICES) {
|
|
245
|
+
next.set(entry.symbol, { quote: null, loading: false, error: message });
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
});
|
|
249
|
+
});
|
|
211
250
|
}, [dataProvider]);
|
|
212
251
|
|
|
213
252
|
useEffect(() => {
|
|
@@ -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
|
},
|