gloomberb 0.7.2 → 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 +21 -3
- 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 +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/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/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
|
@@ -9,7 +9,8 @@ import { cloneLayout, createDefaultConfig, type AppConfig } from "../../types/co
|
|
|
9
9
|
import type { DataProvider } from "../../types/data-provider";
|
|
10
10
|
import type { TickerRecord } from "../../types/ticker";
|
|
11
11
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
12
|
-
import type { PaneTemplateCreateOptions } from "../../types/plugin";
|
|
12
|
+
import type { PaneSettingField, PaneTemplateCreateOptions } from "../../types/plugin";
|
|
13
|
+
import { useShortcut } from "../../react/input";
|
|
13
14
|
|
|
14
15
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
15
16
|
|
|
@@ -169,8 +170,9 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
|
|
|
169
170
|
id: "quote-monitor-pane",
|
|
170
171
|
paneId: "quote-monitor",
|
|
171
172
|
label: "Quote Monitor",
|
|
172
|
-
description: "Open a compact quote monitor for
|
|
173
|
-
shortcut: { prefix: "QQ", argPlaceholder: "ticker" },
|
|
173
|
+
description: "Open a compact quote monitor for one or more tickers",
|
|
174
|
+
shortcut: { prefix: "QQ", argPlaceholder: "tickers", argKind: "ticker-list" },
|
|
175
|
+
wizard: [{ key: "tickers", label: "Quote Tickers", type: "text" }],
|
|
174
176
|
}],
|
|
175
177
|
["new-ibkr-trading-pane", {
|
|
176
178
|
id: "new-ibkr-trading-pane",
|
|
@@ -232,10 +234,53 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
|
|
|
232
234
|
} as unknown as PluginRegistry;
|
|
233
235
|
}
|
|
234
236
|
|
|
237
|
+
function makeQuoteMonitorPaneSettingsDescriptor(
|
|
238
|
+
pluginRegistry: PluginRegistry,
|
|
239
|
+
fields: PaneSettingField[],
|
|
240
|
+
settings: Record<string, unknown> = {},
|
|
241
|
+
) {
|
|
242
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
243
|
+
const pane = {
|
|
244
|
+
instanceId: "quote-monitor:main",
|
|
245
|
+
paneId: "quote-monitor",
|
|
246
|
+
title: "Quote Monitor",
|
|
247
|
+
settings,
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
paneId: "quote-monitor:main",
|
|
251
|
+
pane,
|
|
252
|
+
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
253
|
+
settingsDef: {
|
|
254
|
+
title: "Quote Monitor Settings",
|
|
255
|
+
fields,
|
|
256
|
+
},
|
|
257
|
+
context: {
|
|
258
|
+
config,
|
|
259
|
+
layout: cloneLayout(config.layout),
|
|
260
|
+
paneId: "quote-monitor:main",
|
|
261
|
+
paneType: "quote-monitor",
|
|
262
|
+
pane,
|
|
263
|
+
settings,
|
|
264
|
+
paneState: {},
|
|
265
|
+
activeTicker: "AAPL",
|
|
266
|
+
activeCollectionId: "main",
|
|
267
|
+
},
|
|
268
|
+
} as any;
|
|
269
|
+
}
|
|
270
|
+
|
|
235
271
|
function ThemeProbe() {
|
|
236
272
|
return <text>{`theme:${useThemeId()}`}</text>;
|
|
237
273
|
}
|
|
238
274
|
|
|
275
|
+
function UnhandledEnterProbe({ onEnter }: { onEnter: () => void }) {
|
|
276
|
+
useShortcut((event) => {
|
|
277
|
+
if (event.name === "enter" || event.name === "return") {
|
|
278
|
+
onEnter();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
239
284
|
function CommandBarHarness({
|
|
240
285
|
query,
|
|
241
286
|
disabledPlugins = [],
|
|
@@ -251,6 +296,7 @@ function CommandBarHarness({
|
|
|
251
296
|
hasPaneSettings,
|
|
252
297
|
onCheckForUpdates,
|
|
253
298
|
onAction,
|
|
299
|
+
onUnhandledEnter,
|
|
254
300
|
}: {
|
|
255
301
|
query: string;
|
|
256
302
|
disabledPlugins?: string[];
|
|
@@ -266,6 +312,7 @@ function CommandBarHarness({
|
|
|
266
312
|
hasPaneSettings?: (paneId: string) => boolean;
|
|
267
313
|
onCheckForUpdates?: () => void | Promise<void>;
|
|
268
314
|
onAction?: (action: AppAction) => void;
|
|
315
|
+
onUnhandledEnter?: () => void;
|
|
269
316
|
}) {
|
|
270
317
|
let config = {
|
|
271
318
|
...createDefaultConfig("/tmp/gloomberb-test"),
|
|
@@ -330,6 +377,7 @@ function CommandBarHarness({
|
|
|
330
377
|
onCheckForUpdates={onCheckForUpdates}
|
|
331
378
|
/>
|
|
332
379
|
)}
|
|
380
|
+
{onUnhandledEnter && <UnhandledEnterProbe onEnter={onUnhandledEnter} />}
|
|
333
381
|
</TestDialogProvider>
|
|
334
382
|
</AppContext>
|
|
335
383
|
</ThemeProvider>
|
|
@@ -966,7 +1014,7 @@ describe("CommandBar", () => {
|
|
|
966
1014
|
expect(frame).not.toContain("Back");
|
|
967
1015
|
});
|
|
968
1016
|
|
|
969
|
-
test("QQ without an active ticker opens inline ticker
|
|
1017
|
+
test("QQ without an active ticker opens inline ticker-list entry on enter", async () => {
|
|
970
1018
|
testSetup = await testRender(<CommandBarHarness query="QQ" />, {
|
|
971
1019
|
width: 100,
|
|
972
1020
|
height: 20,
|
|
@@ -981,6 +1029,25 @@ describe("CommandBar", () => {
|
|
|
981
1029
|
await testSetup!.renderOnce();
|
|
982
1030
|
});
|
|
983
1031
|
|
|
1032
|
+
const frame = testSetup.captureCharFrame();
|
|
1033
|
+
expect(frame).toContain("Back");
|
|
1034
|
+
expect(frame).toContain("Quote Tickers");
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test("T without an active ticker opens ticker search on enter", async () => {
|
|
1038
|
+
testSetup = await testRender(<CommandBarHarness query="T" />, {
|
|
1039
|
+
width: 100,
|
|
1040
|
+
height: 20,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
await testSetup.renderOnce();
|
|
1044
|
+
expect(testSetup.captureCharFrame()).toContain("Description");
|
|
1045
|
+
|
|
1046
|
+
await act(async () => {
|
|
1047
|
+
testSetup!.mockInput.pressEnter();
|
|
1048
|
+
await testSetup!.renderOnce();
|
|
1049
|
+
});
|
|
1050
|
+
|
|
984
1051
|
const frame = testSetup.captureCharFrame();
|
|
985
1052
|
expect(frame).toContain("Back");
|
|
986
1053
|
expect(frame).toContain("Security Description");
|
|
@@ -1036,12 +1103,43 @@ describe("CommandBar", () => {
|
|
|
1036
1103
|
templateId: "quote-monitor-pane",
|
|
1037
1104
|
options: {
|
|
1038
1105
|
arg: "AAPL",
|
|
1039
|
-
|
|
1040
|
-
ticker: makeTicker("AAPL", "Apple Inc."),
|
|
1106
|
+
symbols: ["AAPL"],
|
|
1041
1107
|
},
|
|
1042
1108
|
}]);
|
|
1043
1109
|
});
|
|
1044
1110
|
|
|
1111
|
+
test("consumes enter before focused pane shortcuts when executing a pane shortcut", async () => {
|
|
1112
|
+
const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
|
|
1113
|
+
let leakedEnterCount = 0;
|
|
1114
|
+
|
|
1115
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1116
|
+
query="PF"
|
|
1117
|
+
live
|
|
1118
|
+
onUnhandledEnter={() => {
|
|
1119
|
+
leakedEnterCount += 1;
|
|
1120
|
+
}}
|
|
1121
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1122
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
1123
|
+
created.push({ templateId, options });
|
|
1124
|
+
};
|
|
1125
|
+
}}
|
|
1126
|
+
/>, {
|
|
1127
|
+
width: 100,
|
|
1128
|
+
height: 20,
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
await testSetup.renderOnce();
|
|
1132
|
+
|
|
1133
|
+
await act(async () => {
|
|
1134
|
+
testSetup!.mockInput.pressEnter();
|
|
1135
|
+
await Bun.sleep(0);
|
|
1136
|
+
await testSetup!.renderOnce();
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
expect(created).toEqual([{ templateId: "new-portfolio-pane", options: undefined }]);
|
|
1140
|
+
expect(leakedEnterCount).toBe(0);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1045
1143
|
test("typing a chat channel shortcut opens that channel directly", async () => {
|
|
1046
1144
|
const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
|
|
1047
1145
|
|
|
@@ -1134,6 +1232,121 @@ describe("CommandBar", () => {
|
|
|
1134
1232
|
expect(frame).toContain("AAOI");
|
|
1135
1233
|
});
|
|
1136
1234
|
|
|
1235
|
+
test("keeps exact T ticker matches above noisy provider matches", async () => {
|
|
1236
|
+
testSetup = await testRender(
|
|
1237
|
+
<CommandBarHarness
|
|
1238
|
+
query="T AMD"
|
|
1239
|
+
extraTickers={[
|
|
1240
|
+
makeTicker("AMDL", "GraniteShares 2x Long AMD Daily"),
|
|
1241
|
+
makeTicker("AMUU", "Direxion Daily AMD Bull 2X ETF"),
|
|
1242
|
+
]}
|
|
1243
|
+
dataProvider={makeDataProvider(async () => [
|
|
1244
|
+
{ providerId: "yahoo", symbol: "DES", name: "AMD-themed synthetic listing", exchange: "ASX", type: "EQUITY" },
|
|
1245
|
+
{ providerId: "yahoo", symbol: "DES2", name: "AMD-themed secondary listing", exchange: "LSE", type: "EQUITY" },
|
|
1246
|
+
{ providerId: "yahoo", symbol: "AMD", name: "Advanced Micro Devices", exchange: "NASDAQ", type: "EQUITY" },
|
|
1247
|
+
])}
|
|
1248
|
+
/>,
|
|
1249
|
+
{ width: 100, height: 24 },
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
await testSetup.renderOnce();
|
|
1253
|
+
const frame = await waitForFrameToContain("DES2");
|
|
1254
|
+
const resultLines = frame
|
|
1255
|
+
.split("\n")
|
|
1256
|
+
.filter((line) => /\b(AMD|DES2?|DES)\b/.test(line) && /\b(NASDAQ|ASX|LSE)\b/.test(line));
|
|
1257
|
+
|
|
1258
|
+
expect(resultLines[0]).toContain("AMD");
|
|
1259
|
+
expect(resultLines[0]).toContain("NASDAQ");
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("loads exact provider matches for plain ticker-shaped root queries", async () => {
|
|
1263
|
+
testSetup = await testRender(
|
|
1264
|
+
<CommandBarHarness
|
|
1265
|
+
query="amd"
|
|
1266
|
+
extraTickers={[
|
|
1267
|
+
makeTicker("AMDL", "GraniteShares 2x Long AMD Daily"),
|
|
1268
|
+
makeTicker("AMUU", "Direxion Daily AMD Bull 2X ETF"),
|
|
1269
|
+
]}
|
|
1270
|
+
dataProvider={makeDataProvider(async () => [
|
|
1271
|
+
{ providerId: "yahoo", symbol: "DES", name: "AMD-themed synthetic listing", exchange: "ASX", type: "EQUITY" },
|
|
1272
|
+
{ providerId: "yahoo", symbol: "AMD", name: "Advanced Micro Devices", exchange: "NASDAQ", type: "EQUITY" },
|
|
1273
|
+
])}
|
|
1274
|
+
/>,
|
|
1275
|
+
{ width: 100, height: 24 },
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
await testSetup.renderOnce();
|
|
1279
|
+
const frame = await waitForFrameToContain("DES");
|
|
1280
|
+
const resultLines = frame
|
|
1281
|
+
.split("\n")
|
|
1282
|
+
.filter((line) => /\b(AMD|AMDL|AMUU|DES)\b/.test(line) && /\b(NASDAQ|ASX)\b/.test(line));
|
|
1283
|
+
|
|
1284
|
+
expect(resultLines[0]).toContain("AMD");
|
|
1285
|
+
expect(resultLines[0]).toContain("NASDAQ");
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
test("keeps pane matches visible alongside plain ticker provider results", async () => {
|
|
1289
|
+
testSetup = await testRender(
|
|
1290
|
+
<CommandBarHarness
|
|
1291
|
+
query="opti"
|
|
1292
|
+
dataProvider={makeDataProvider(async () => [
|
|
1293
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "CSE", type: "EQUITY" },
|
|
1294
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "NEO", type: "EQUITY" },
|
|
1295
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "LSE", type: "EQUITY" },
|
|
1296
|
+
])}
|
|
1297
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1298
|
+
const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
|
|
1299
|
+
paneTemplates.set("options-pane", {
|
|
1300
|
+
id: "options-pane",
|
|
1301
|
+
paneId: "options",
|
|
1302
|
+
label: "Options",
|
|
1303
|
+
description: "Options chain for the selected ticker",
|
|
1304
|
+
shortcut: { prefix: "OMON", argPlaceholder: "ticker", argKind: "ticker" },
|
|
1305
|
+
canCreate: (context: any, options?: PaneTemplateCreateOptions) => (
|
|
1306
|
+
context.activeTicker !== null || !!options?.arg
|
|
1307
|
+
),
|
|
1308
|
+
});
|
|
1309
|
+
}}
|
|
1310
|
+
/>,
|
|
1311
|
+
{ width: 100, height: 24 },
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
await testSetup.renderOnce();
|
|
1315
|
+
const frame = await waitForFrameToContain("OPTI");
|
|
1316
|
+
expect(frame).toContain("Options");
|
|
1317
|
+
expect(frame).toContain("OMON");
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("clears stale provider rows after a plain ticker root search is cleared", async () => {
|
|
1321
|
+
testSetup = await testRender(
|
|
1322
|
+
<CommandBarHarness
|
|
1323
|
+
query=""
|
|
1324
|
+
live
|
|
1325
|
+
dataProvider={makeDataProvider(async () => [
|
|
1326
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "CSE", type: "EQUITY" },
|
|
1327
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "NEO", type: "EQUITY" },
|
|
1328
|
+
{ providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "LSE", type: "EQUITY" },
|
|
1329
|
+
])}
|
|
1330
|
+
/>,
|
|
1331
|
+
{ width: 100, height: 24 },
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
await testSetup.renderOnce();
|
|
1335
|
+
await act(async () => {
|
|
1336
|
+
await testSetup!.mockInput.typeText("opti");
|
|
1337
|
+
await testSetup!.renderOnce();
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
await waitForFrameToContain("OPTI");
|
|
1341
|
+
await emitKeypress(testSetup, { name: "u", ctrl: true });
|
|
1342
|
+
await renderFrames(2);
|
|
1343
|
+
|
|
1344
|
+
const frame = testSetup.captureCharFrame();
|
|
1345
|
+
expect(frame).toContain("Search");
|
|
1346
|
+
expect(frame).toContain("Tickers");
|
|
1347
|
+
expect(frame).not.toContain("OPTI");
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1137
1350
|
test("DES MSFT opens an exact ticker directly", async () => {
|
|
1138
1351
|
const pinned: string[] = [];
|
|
1139
1352
|
|
|
@@ -1160,6 +1373,33 @@ describe("CommandBar", () => {
|
|
|
1160
1373
|
expect(pinned).toEqual(["MSFT"]);
|
|
1161
1374
|
});
|
|
1162
1375
|
|
|
1376
|
+
test("T AMD opens an exact ticker directly", async () => {
|
|
1377
|
+
const pinned: string[] = [];
|
|
1378
|
+
|
|
1379
|
+
testSetup = await testRender(
|
|
1380
|
+
<CommandBarHarness
|
|
1381
|
+
query="T AMD"
|
|
1382
|
+
extraTickers={[makeTicker("AMD", "Advanced Micro Devices")]}
|
|
1383
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1384
|
+
pluginRegistry.pinTicker = (symbol) => {
|
|
1385
|
+
pinned.push(symbol);
|
|
1386
|
+
};
|
|
1387
|
+
}}
|
|
1388
|
+
/>,
|
|
1389
|
+
{ width: 100, height: 20 },
|
|
1390
|
+
);
|
|
1391
|
+
|
|
1392
|
+
await testSetup.renderOnce();
|
|
1393
|
+
await act(async () => {
|
|
1394
|
+
testSetup!.mockInput.pressEnter();
|
|
1395
|
+
await Bun.sleep(0);
|
|
1396
|
+
await testSetup!.renderOnce();
|
|
1397
|
+
await testSetup!.renderOnce();
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
expect(pinned).toEqual(["AMD"]);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1163
1403
|
test("AW AAPL uses the active watchlist target by default", async () => {
|
|
1164
1404
|
const saved: TickerRecord[] = [];
|
|
1165
1405
|
|
|
@@ -1897,6 +2137,32 @@ describe("CommandBar", () => {
|
|
|
1897
2137
|
expect(frame).toContain("Current Layout");
|
|
1898
2138
|
});
|
|
1899
2139
|
|
|
2140
|
+
test("runs layout actions directly from root search", async () => {
|
|
2141
|
+
const actions: AppAction[] = [];
|
|
2142
|
+
|
|
2143
|
+
testSetup = await testRender(<CommandBarHarness
|
|
2144
|
+
query="undo layout change"
|
|
2145
|
+
live
|
|
2146
|
+
configureConfig={layoutModeConfig}
|
|
2147
|
+
configureState={layoutModeState}
|
|
2148
|
+
onAction={(action) => actions.push(action)}
|
|
2149
|
+
/>, {
|
|
2150
|
+
width: 90,
|
|
2151
|
+
height: 24,
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
await testSetup.renderOnce();
|
|
2155
|
+
|
|
2156
|
+
expect(testSetup.captureCharFrame()).toContain("Undo Layout Change");
|
|
2157
|
+
|
|
2158
|
+
await act(async () => {
|
|
2159
|
+
testSetup!.mockInput.pressEnter();
|
|
2160
|
+
await testSetup!.renderOnce();
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
expect(actions.some((action) => action.type === "UNDO_LAYOUT")).toBe(true);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
1900
2166
|
test("renders filtered saved layouts with textual previews", async () => {
|
|
1901
2167
|
testSetup = await testRender(<CommandBarHarness
|
|
1902
2168
|
query="LAY Research"
|
|
@@ -2028,6 +2294,66 @@ describe("CommandBar", () => {
|
|
|
2028
2294
|
expect(created).toEqual([{ templateId: "news-top-pane", options: undefined }]);
|
|
2029
2295
|
});
|
|
2030
2296
|
|
|
2297
|
+
test("surfaces ticker-required pane templates by name without an active ticker", async () => {
|
|
2298
|
+
testSetup = await testRender(<CommandBarHarness
|
|
2299
|
+
query="Options"
|
|
2300
|
+
extraTickers={[makeTicker("^RUT", "Chicago Board Options Exchange Russell 2000")]}
|
|
2301
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
2302
|
+
const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
|
|
2303
|
+
paneTemplates.set("options-pane", {
|
|
2304
|
+
id: "options-pane",
|
|
2305
|
+
paneId: "options",
|
|
2306
|
+
label: "Options",
|
|
2307
|
+
description: "Options chain for the selected ticker",
|
|
2308
|
+
keywords: ["options", "chain", "calls", "puts"],
|
|
2309
|
+
shortcut: { prefix: "OMON", argPlaceholder: "ticker", argKind: "ticker" },
|
|
2310
|
+
canCreate: (context: any, options?: PaneTemplateCreateOptions) => (
|
|
2311
|
+
context.activeTicker !== null || !!options?.arg
|
|
2312
|
+
),
|
|
2313
|
+
});
|
|
2314
|
+
}}
|
|
2315
|
+
/>, {
|
|
2316
|
+
width: 100,
|
|
2317
|
+
height: 18,
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
await testSetup.renderOnce();
|
|
2321
|
+
|
|
2322
|
+
let frame = testSetup.captureCharFrame();
|
|
2323
|
+
expect(frame).toContain("Options");
|
|
2324
|
+
expect(frame).toContain("OMON");
|
|
2325
|
+
|
|
2326
|
+
await act(async () => {
|
|
2327
|
+
testSetup!.mockInput.pressEnter();
|
|
2328
|
+
await testSetup!.renderOnce();
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
frame = testSetup.captureCharFrame();
|
|
2332
|
+
expect(frame).toContain("Back");
|
|
2333
|
+
expect(frame).toContain("Ticker");
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
test("ranks exact ticker symbols before longer ticker prefixes in root search", async () => {
|
|
2337
|
+
testSetup = await testRender(<CommandBarHarness
|
|
2338
|
+
query="amd"
|
|
2339
|
+
extraTickers={[
|
|
2340
|
+
makeTicker("AMDL", "GraniteShares 2x Long AMD Daily ETF"),
|
|
2341
|
+
makeTicker("AMD", "Advanced Micro Devices"),
|
|
2342
|
+
]}
|
|
2343
|
+
/>, {
|
|
2344
|
+
width: 100,
|
|
2345
|
+
height: 18,
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
await testSetup.renderOnce();
|
|
2349
|
+
|
|
2350
|
+
const tickerLines = testSetup.captureCharFrame()
|
|
2351
|
+
.split("\n")
|
|
2352
|
+
.filter((line) => /\bAMD\b|\bAMDL\b/.test(line));
|
|
2353
|
+
expect(tickerLines[0]).toMatch(/\bAMD\b/);
|
|
2354
|
+
expect(tickerLines[0]).not.toMatch(/\bAMDL\b/);
|
|
2355
|
+
});
|
|
2356
|
+
|
|
2031
2357
|
test("opens plugin command wizards inline inside the command bar", async () => {
|
|
2032
2358
|
let submittedValues: Record<string, string> | undefined;
|
|
2033
2359
|
|
|
@@ -2199,8 +2525,39 @@ describe("CommandBar", () => {
|
|
|
2199
2525
|
templateId: "quote-monitor-pane",
|
|
2200
2526
|
options: {
|
|
2201
2527
|
arg: "MSFT",
|
|
2202
|
-
|
|
2203
|
-
|
|
2528
|
+
symbols: ["MSFT"],
|
|
2529
|
+
},
|
|
2530
|
+
}]);
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
test("QQ AAPL,MSFT creates a multi-symbol quote monitor directly", async () => {
|
|
2534
|
+
const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
|
|
2535
|
+
|
|
2536
|
+
testSetup = await testRender(<CommandBarHarness
|
|
2537
|
+
query="QQ AAPL,MSFT"
|
|
2538
|
+
selectedTicker="AAPL"
|
|
2539
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
2540
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
2541
|
+
created.push({ templateId, options });
|
|
2542
|
+
};
|
|
2543
|
+
}}
|
|
2544
|
+
/>, {
|
|
2545
|
+
width: 100,
|
|
2546
|
+
height: 20,
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
await testSetup.renderOnce();
|
|
2550
|
+
await act(async () => {
|
|
2551
|
+
testSetup!.mockInput.pressEnter();
|
|
2552
|
+
await Bun.sleep(0);
|
|
2553
|
+
await testSetup!.renderOnce();
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
expect(created).toEqual([{
|
|
2557
|
+
templateId: "quote-monitor-pane",
|
|
2558
|
+
options: {
|
|
2559
|
+
arg: "AAPL,MSFT",
|
|
2560
|
+
symbols: ["AAPL", "MSFT"],
|
|
2204
2561
|
},
|
|
2205
2562
|
}]);
|
|
2206
2563
|
});
|
|
@@ -2469,41 +2826,12 @@ describe("CommandBar", () => {
|
|
|
2469
2826
|
})}
|
|
2470
2827
|
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
2471
2828
|
configurePluginRegistry={(pluginRegistry) => {
|
|
2472
|
-
pluginRegistry.resolvePaneSettings = () => ({
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
settings: {},
|
|
2479
|
-
},
|
|
2480
|
-
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
2481
|
-
settingsDef: {
|
|
2482
|
-
title: "Quote Monitor Settings",
|
|
2483
|
-
fields: [{
|
|
2484
|
-
key: "symbol",
|
|
2485
|
-
label: "Symbol",
|
|
2486
|
-
type: "text",
|
|
2487
|
-
description: "Ticker symbol to track",
|
|
2488
|
-
}],
|
|
2489
|
-
},
|
|
2490
|
-
context: {
|
|
2491
|
-
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
2492
|
-
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
2493
|
-
paneId: "quote-monitor:main",
|
|
2494
|
-
paneType: "quote-monitor",
|
|
2495
|
-
pane: {
|
|
2496
|
-
instanceId: "quote-monitor:main",
|
|
2497
|
-
paneId: "quote-monitor",
|
|
2498
|
-
title: "Quote Monitor",
|
|
2499
|
-
settings: {},
|
|
2500
|
-
},
|
|
2501
|
-
settings: {},
|
|
2502
|
-
paneState: {},
|
|
2503
|
-
activeTicker: "AAPL",
|
|
2504
|
-
activeCollectionId: "main",
|
|
2505
|
-
},
|
|
2506
|
-
}) as any;
|
|
2829
|
+
pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
|
|
2830
|
+
key: "symbol",
|
|
2831
|
+
label: "Symbol",
|
|
2832
|
+
type: "text",
|
|
2833
|
+
description: "Ticker symbol to track",
|
|
2834
|
+
}]);
|
|
2507
2835
|
pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
|
|
2508
2836
|
appliedValues.push({ paneId, key: field.key, value });
|
|
2509
2837
|
};
|
|
@@ -2550,6 +2878,64 @@ describe("CommandBar", () => {
|
|
|
2550
2878
|
}]);
|
|
2551
2879
|
});
|
|
2552
2880
|
|
|
2881
|
+
test("opens focused pane settings directly from root search", async () => {
|
|
2882
|
+
const appliedValues: Array<{ paneId: string; key: string; value: unknown }> = [];
|
|
2883
|
+
|
|
2884
|
+
testSetup = await testRender(<CommandBarHarness
|
|
2885
|
+
query="ticker symbol"
|
|
2886
|
+
configureState={(state) => ({
|
|
2887
|
+
...state,
|
|
2888
|
+
focusedPaneId: "quote-monitor:main",
|
|
2889
|
+
})}
|
|
2890
|
+
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
2891
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
2892
|
+
pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
|
|
2893
|
+
key: "symbol",
|
|
2894
|
+
label: "Symbol",
|
|
2895
|
+
type: "text",
|
|
2896
|
+
description: "Ticker symbol to track",
|
|
2897
|
+
}]);
|
|
2898
|
+
pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
|
|
2899
|
+
appliedValues.push({ paneId, key: field.key, value });
|
|
2900
|
+
};
|
|
2901
|
+
}}
|
|
2902
|
+
/>, {
|
|
2903
|
+
width: 100,
|
|
2904
|
+
height: 20,
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
await testSetup.renderOnce();
|
|
2908
|
+
|
|
2909
|
+
let frame = testSetup.captureCharFrame();
|
|
2910
|
+
expect(frame).toContain("Quote Monitor Settings");
|
|
2911
|
+
expect(frame).toContain("Symbol");
|
|
2912
|
+
|
|
2913
|
+
await act(async () => {
|
|
2914
|
+
testSetup!.mockInput.pressEnter();
|
|
2915
|
+
await testSetup!.renderOnce();
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
frame = testSetup.captureCharFrame();
|
|
2919
|
+
expect(frame).toContain("Apply");
|
|
2920
|
+
expect(frame).toContain("Symbol");
|
|
2921
|
+
|
|
2922
|
+
await act(async () => {
|
|
2923
|
+
await testSetup!.mockInput.typeText("MSFT");
|
|
2924
|
+
await testSetup!.renderOnce();
|
|
2925
|
+
});
|
|
2926
|
+
await act(async () => {
|
|
2927
|
+
testSetup!.mockInput.pressEnter();
|
|
2928
|
+
await Bun.sleep(0);
|
|
2929
|
+
await testSetup!.renderOnce();
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
expect(appliedValues).toEqual([{
|
|
2933
|
+
paneId: "quote-monitor:main",
|
|
2934
|
+
key: "symbol",
|
|
2935
|
+
value: "MSFT",
|
|
2936
|
+
}]);
|
|
2937
|
+
});
|
|
2938
|
+
|
|
2553
2939
|
test("uses backspace as back only when a pane-settings route query is empty", async () => {
|
|
2554
2940
|
testSetup = await testRender(<CommandBarHarness
|
|
2555
2941
|
query="PS"
|
|
@@ -2559,41 +2945,12 @@ describe("CommandBar", () => {
|
|
|
2559
2945
|
})}
|
|
2560
2946
|
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
2561
2947
|
configurePluginRegistry={(pluginRegistry) => {
|
|
2562
|
-
pluginRegistry.resolvePaneSettings = () => ({
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
settings: {},
|
|
2569
|
-
},
|
|
2570
|
-
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
2571
|
-
settingsDef: {
|
|
2572
|
-
title: "Quote Monitor Settings",
|
|
2573
|
-
fields: [{
|
|
2574
|
-
key: "symbol",
|
|
2575
|
-
label: "Symbol",
|
|
2576
|
-
type: "text",
|
|
2577
|
-
description: "Ticker symbol to track",
|
|
2578
|
-
}],
|
|
2579
|
-
},
|
|
2580
|
-
context: {
|
|
2581
|
-
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
2582
|
-
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
2583
|
-
paneId: "quote-monitor:main",
|
|
2584
|
-
paneType: "quote-monitor",
|
|
2585
|
-
pane: {
|
|
2586
|
-
instanceId: "quote-monitor:main",
|
|
2587
|
-
paneId: "quote-monitor",
|
|
2588
|
-
title: "Quote Monitor",
|
|
2589
|
-
settings: {},
|
|
2590
|
-
},
|
|
2591
|
-
settings: {},
|
|
2592
|
-
paneState: {},
|
|
2593
|
-
activeTicker: "AAPL",
|
|
2594
|
-
activeCollectionId: "main",
|
|
2595
|
-
},
|
|
2596
|
-
}) as any;
|
|
2948
|
+
pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
|
|
2949
|
+
key: "symbol",
|
|
2950
|
+
label: "Symbol",
|
|
2951
|
+
type: "text",
|
|
2952
|
+
description: "Ticker symbol to track",
|
|
2953
|
+
}]);
|
|
2597
2954
|
}}
|
|
2598
2955
|
/>, {
|
|
2599
2956
|
width: 100,
|
|
@@ -2649,48 +3006,17 @@ describe("CommandBar", () => {
|
|
|
2649
3006
|
})}
|
|
2650
3007
|
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
2651
3008
|
configurePluginRegistry={(pluginRegistry) => {
|
|
2652
|
-
pluginRegistry.resolvePaneSettings = () => ({
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
fields: [{
|
|
2664
|
-
key: "columns",
|
|
2665
|
-
label: "Columns",
|
|
2666
|
-
type: "ordered-multi-select",
|
|
2667
|
-
description: "Visible columns",
|
|
2668
|
-
options: [
|
|
2669
|
-
{ value: "volume", label: "AAA", description: "Volume column" },
|
|
2670
|
-
{ value: "spread", label: "BBB", description: "Spread column" },
|
|
2671
|
-
{ value: "beta", label: "CCC", description: "Beta column" },
|
|
2672
|
-
],
|
|
2673
|
-
}],
|
|
2674
|
-
},
|
|
2675
|
-
context: {
|
|
2676
|
-
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
2677
|
-
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
2678
|
-
paneId: "quote-monitor:main",
|
|
2679
|
-
paneType: "quote-monitor",
|
|
2680
|
-
pane: {
|
|
2681
|
-
instanceId: "quote-monitor:main",
|
|
2682
|
-
paneId: "quote-monitor",
|
|
2683
|
-
title: "Quote Monitor",
|
|
2684
|
-
settings: {},
|
|
2685
|
-
},
|
|
2686
|
-
settings: {
|
|
2687
|
-
columns: ["volume", "spread"],
|
|
2688
|
-
},
|
|
2689
|
-
paneState: {},
|
|
2690
|
-
activeTicker: "AAPL",
|
|
2691
|
-
activeCollectionId: "main",
|
|
2692
|
-
},
|
|
2693
|
-
}) as any;
|
|
3009
|
+
pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
|
|
3010
|
+
key: "columns",
|
|
3011
|
+
label: "Columns",
|
|
3012
|
+
type: "ordered-multi-select",
|
|
3013
|
+
description: "Visible columns",
|
|
3014
|
+
options: [
|
|
3015
|
+
{ value: "volume", label: "AAA", description: "Volume column" },
|
|
3016
|
+
{ value: "spread", label: "BBB", description: "Spread column" },
|
|
3017
|
+
{ value: "beta", label: "CCC", description: "Beta column" },
|
|
3018
|
+
],
|
|
3019
|
+
}], { columns: ["volume", "spread"] });
|
|
2694
3020
|
}}
|
|
2695
3021
|
/>, {
|
|
2696
3022
|
width: 100,
|