gloomberb 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -44,6 +44,9 @@ export function createRemoteBrokerAdapter(adapter: BrokerAdapter): BrokerAdapter
|
|
|
44
44
|
...adapter,
|
|
45
45
|
validate: (instance) => invoke<boolean>(instance, "validate"),
|
|
46
46
|
importPositions: (instance) => invoke(instance, "importPositions"),
|
|
47
|
+
importPortfolioSnapshot: adapter.importPortfolioSnapshot
|
|
48
|
+
? (instance) => invoke(instance, "importPortfolioSnapshot")
|
|
49
|
+
: undefined,
|
|
47
50
|
connect: (instance) => invoke<void>(instance, "connect"),
|
|
48
51
|
disconnect: (instance) => invoke<void>(instance, "disconnect"),
|
|
49
52
|
getStatus: (instance) => getBrokerRemoteClient()?.getStatus(instance.id) ?? adapter.getStatus?.(instance) ?? disconnectedStatus(),
|
|
@@ -47,6 +47,10 @@ describe("CapabilityRegistry", () => {
|
|
|
47
47
|
|
|
48
48
|
dispose();
|
|
49
49
|
expect(registry.list()).toEqual([]);
|
|
50
|
+
|
|
51
|
+
registry.register("plugin-b", testCapability());
|
|
52
|
+
dispose();
|
|
53
|
+
expect(registry.list()).toHaveLength(1);
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
test("filters disabled plugins and disabled capabilities", () => {
|
|
@@ -134,4 +138,25 @@ describe("CapabilityRegistry", () => {
|
|
|
134
138
|
expect(disposed).toBe(2);
|
|
135
139
|
expect(registry.list()).toEqual([]);
|
|
136
140
|
});
|
|
141
|
+
|
|
142
|
+
test("disposes subscriptions that finish after their capability is removed", async () => {
|
|
143
|
+
const registry = new CapabilityRegistry();
|
|
144
|
+
let finishSubscribe!: (dispose: () => void) => void;
|
|
145
|
+
let disposed = 0;
|
|
146
|
+
const disposeCapability = registry.register("plugin-a", testCapability({
|
|
147
|
+
operations: {
|
|
148
|
+
ticks: {
|
|
149
|
+
kind: "stream",
|
|
150
|
+
subscribe: () => new Promise((resolve) => { finishSubscribe = resolve; }),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
const subscribing = registry.subscribe("plugin-service.test", "ticks", {}, () => {});
|
|
156
|
+
disposeCapability();
|
|
157
|
+
finishSubscribe(() => { disposed += 1; });
|
|
158
|
+
|
|
159
|
+
await expect(subscribing).rejects.toThrow("not available");
|
|
160
|
+
expect(disposed).toBe(1);
|
|
161
|
+
});
|
|
137
162
|
});
|
|
@@ -24,15 +24,22 @@ export class CapabilityRegistry {
|
|
|
24
24
|
if (existing) {
|
|
25
25
|
throw new Error(`Capability "${capability.id}" already registered by plugin "${existing.pluginId}".`);
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
const registration = { pluginId, capability };
|
|
28
|
+
this.capabilities.set(capability.id, registration);
|
|
28
29
|
return () => {
|
|
30
|
+
if (this.capabilities.get(capability.id) !== registration) return;
|
|
29
31
|
this.capabilities.delete(capability.id);
|
|
32
|
+
let firstError: unknown;
|
|
30
33
|
for (const [subscriptionId, entry] of this.subscriptions) {
|
|
31
34
|
if (entry.capabilityId === capability.id) {
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
try {
|
|
36
|
+
this.unsubscribe(subscriptionId);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
firstError ??= error;
|
|
39
|
+
}
|
|
34
40
|
}
|
|
35
41
|
}
|
|
42
|
+
if (firstError) throw firstError;
|
|
36
43
|
};
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -97,6 +104,10 @@ export class CapabilityRegistry {
|
|
|
97
104
|
const subscriptionId = options.subscriptionId ?? `${capabilityId}:${operationId}:${this.nextSubscriptionId++}`;
|
|
98
105
|
this.unsubscribe(subscriptionId);
|
|
99
106
|
const dispose = await operation.subscribe(input, emit, { capability, operationId });
|
|
107
|
+
if (this.capabilities.get(capabilityId)?.capability !== capability) {
|
|
108
|
+
dispose();
|
|
109
|
+
throw new Error(`Capability "${capabilityId}" is not available.`);
|
|
110
|
+
}
|
|
100
111
|
this.subscriptions.set(subscriptionId, { capabilityId, dispose });
|
|
101
112
|
return subscriptionId;
|
|
102
113
|
}
|
|
@@ -104,12 +115,18 @@ export class CapabilityRegistry {
|
|
|
104
115
|
unsubscribe(subscriptionId: string): void {
|
|
105
116
|
const entry = this.subscriptions.get(subscriptionId);
|
|
106
117
|
if (!entry) return;
|
|
107
|
-
entry.dispose();
|
|
108
118
|
this.subscriptions.delete(subscriptionId);
|
|
119
|
+
entry.dispose();
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
destroy(): void {
|
|
112
|
-
for (const subscriptionId of [...this.subscriptions.keys()])
|
|
123
|
+
for (const subscriptionId of [...this.subscriptions.keys()]) {
|
|
124
|
+
try {
|
|
125
|
+
this.unsubscribe(subscriptionId);
|
|
126
|
+
} catch {
|
|
127
|
+
// Continue releasing the remaining subscriptions.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
this.capabilities.clear();
|
|
114
131
|
}
|
|
115
132
|
|
|
@@ -102,9 +102,7 @@ export const aiCliCommand: CliCommandDef = {
|
|
|
102
102
|
if (action === "ask") {
|
|
103
103
|
const rawArgs = args.slice(1);
|
|
104
104
|
const providerId = takeOption(rawArgs, "--provider") ?? detectProviders().find((provider) => provider.available)?.id;
|
|
105
|
-
const
|
|
106
|
-
if (!provider) ctx.fail("Unknown AI provider.");
|
|
107
|
-
const selectedProvider = provider;
|
|
105
|
+
const selectedProvider = getAiProvider(providerId) ?? ctx.fail("Unknown AI provider.");
|
|
108
106
|
if (!selectedProvider.available) ctx.fail(`${selectedProvider.name} is not installed or not available in PATH.`);
|
|
109
107
|
const prompt = rawArgs.join(" ").trim();
|
|
110
108
|
if (!prompt) ctx.fail("Usage: gloomberb ai ask [--provider id] <prompt>");
|
|
@@ -15,13 +15,6 @@ export function takeOption(args: string[], name: string): string | undefined {
|
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function takeFlag(args: string[], name: string): boolean {
|
|
19
|
-
const index = args.indexOf(name);
|
|
20
|
-
if (index < 0) return false;
|
|
21
|
-
args.splice(index, 1);
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
export function parseJsonPayload(value: string | undefined, ctx: CliCommandContext): unknown {
|
|
26
19
|
if (!value) return {};
|
|
27
20
|
try {
|
|
@@ -211,8 +211,8 @@ async function runCorrelation(args: string[], ctx: Parameters<CliCommandDef["exe
|
|
|
211
211
|
|
|
212
212
|
export const overviewCliCommands: CliCommandDef[] = [
|
|
213
213
|
{ name: "movers", description: "Fetch gainers, losers, active, or trending market movers", help: { usage: ["movers [gainers|losers|active|trending]"] }, execute: runMoverCommand },
|
|
214
|
-
{ name: "indices", description: "Fetch major US index quotes", execute: (
|
|
215
|
-
{ name: "sectors", description: "Fetch SPDR sector ETF quotes", execute: (
|
|
214
|
+
{ name: "indices", description: "Fetch major US index quotes", execute: (_args, ctx) => runQuoteBasket([...MARKET_SUMMARY_SYMBOLS], ctx, { group: "indices" }) },
|
|
215
|
+
{ name: "sectors", description: "Fetch SPDR sector ETF quotes", execute: (_args, ctx) => runQuoteBasket(SECTOR_ETFS, ctx, { group: "sectors" }) },
|
|
216
216
|
{ name: "fear-greed", description: "Fetch CNN Fear & Greed gauge data", execute: runFearGreed },
|
|
217
217
|
{ name: "econ", description: "Fetch economic calendar events", help: { usage: ["econ [--country US|G7|EU|all] [--impact high|medium|low|all]"] }, execute: runEcon },
|
|
218
218
|
{ name: "fred", description: "Fetch a FRED series through the configured cloud session", help: { usage: ["fred <series-id> [--start yyyy-mm-dd]"] }, execute: runFred },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
4
|
import { getPluginsDir } from "../../plugins/loader";
|
|
5
5
|
import {
|
|
6
6
|
cliStyles,
|
|
@@ -18,18 +18,34 @@ function ensurePluginsDir() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const GITHUB_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
22
|
+
|
|
23
|
+
function validatePluginDirectoryName(name: string): string {
|
|
24
|
+
if (!GITHUB_SEGMENT_PATTERN.test(name)) {
|
|
25
|
+
throw new Error(`Invalid plugin name: ${name}.`);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseGitHubRef(rawRef: string): { url: string; name: string } {
|
|
31
|
+
const ref = rawRef.startsWith("github:") ? rawRef.slice("github:".length) : rawRef;
|
|
32
|
+
let segments: string[];
|
|
33
|
+
if (ref.startsWith("https://")) {
|
|
34
|
+
const parsed = new URL(ref);
|
|
35
|
+
if (parsed.hostname !== "github.com" || parsed.port || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
36
|
+
throw new Error(`Invalid plugin reference: ${rawRef}. Use user/repo or a GitHub URL.`);
|
|
37
|
+
}
|
|
38
|
+
segments = parsed.pathname.split("/").filter(Boolean);
|
|
39
|
+
} else if (!ref.includes("://")) {
|
|
40
|
+
segments = ref.split("/");
|
|
41
|
+
} else {
|
|
42
|
+
segments = [];
|
|
29
43
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const [owner, rawRepo] = segments;
|
|
45
|
+
const repo = rawRepo?.replace(/\.git$/, "");
|
|
46
|
+
if (segments.length === 2 && owner && GITHUB_SEGMENT_PATTERN.test(owner) && repo) {
|
|
47
|
+
const name = validatePluginDirectoryName(repo);
|
|
48
|
+
return { url: `https://github.com/${owner}/${name}.git`, name };
|
|
33
49
|
}
|
|
34
50
|
throw new Error(`Invalid plugin reference: ${ref}. Use user/repo or a GitHub URL.`);
|
|
35
51
|
}
|
|
@@ -47,8 +63,9 @@ export async function installPlugin(ref: string) {
|
|
|
47
63
|
console.log(cliStyles.muted(url));
|
|
48
64
|
|
|
49
65
|
try {
|
|
50
|
-
|
|
66
|
+
execFileSync("git", ["clone", "--depth", "1", url, targetDir], { stdio: "inherit" });
|
|
51
67
|
} catch {
|
|
68
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
52
69
|
fail(`Failed to clone ${url}.`);
|
|
53
70
|
}
|
|
54
71
|
|
|
@@ -56,7 +73,7 @@ export async function installPlugin(ref: string) {
|
|
|
56
73
|
if (existsSync(pkgPath)) {
|
|
57
74
|
console.log(cliStyles.muted("Installing plugin dependencies..."));
|
|
58
75
|
try {
|
|
59
|
-
|
|
76
|
+
execFileSync("bun", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
60
77
|
} catch {
|
|
61
78
|
console.error(cliStyles.warning("Warning: failed to install plugin dependencies."));
|
|
62
79
|
}
|
|
@@ -92,7 +109,7 @@ export async function installPlugin(ref: string) {
|
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
export async function removePlugin(name: string) {
|
|
95
|
-
const targetDir = join(PLUGINS_DIR, name);
|
|
112
|
+
const targetDir = join(PLUGINS_DIR, validatePluginDirectoryName(name));
|
|
96
113
|
if (!existsSync(targetDir)) {
|
|
97
114
|
fail(`Plugin "${name}" was not found.`, PLUGINS_DIR);
|
|
98
115
|
}
|
|
@@ -103,7 +120,7 @@ export async function removePlugin(name: string) {
|
|
|
103
120
|
export async function updatePlugins(name?: string) {
|
|
104
121
|
ensurePluginsDir();
|
|
105
122
|
const dirs = name
|
|
106
|
-
? [name]
|
|
123
|
+
? [validatePluginDirectoryName(name)]
|
|
107
124
|
: readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
108
125
|
.filter((entry) => entry.isDirectory())
|
|
109
126
|
.map((entry) => entry.name);
|
|
@@ -121,10 +138,10 @@ export async function updatePlugins(name?: string) {
|
|
|
121
138
|
}
|
|
122
139
|
console.log(cliStyles.accent(`Updating ${dir}...`));
|
|
123
140
|
try {
|
|
124
|
-
|
|
141
|
+
execFileSync("git", ["pull", "--ff-only"], { cwd: targetDir, stdio: "inherit" });
|
|
125
142
|
const pkgPath = join(targetDir, "package.json");
|
|
126
143
|
if (existsSync(pkgPath)) {
|
|
127
|
-
|
|
144
|
+
execFileSync("bun", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
128
145
|
}
|
|
129
146
|
} catch {
|
|
130
147
|
console.error(cliStyles.danger(`Failed to update ${dir}.`));
|
package/src/cli/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { fail, inferCliErrorOptions, printCliError } from "./errors";
|
|
15
15
|
import { setCliColorEnabledOverride } from "../utils/cli-output";
|
|
16
16
|
import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
|
|
17
|
-
import { ticker
|
|
17
|
+
import { ticker } from "./commands/ticker";
|
|
18
18
|
import { apiCliCommand } from "./commands/api";
|
|
19
19
|
import { marketDataCliCommands } from "./commands/market";
|
|
20
20
|
import { overviewCliCommands } from "./commands/overview";
|
|
@@ -216,7 +216,7 @@ async function createRegistry(options: DispatchCliOptions = {}): Promise<CliComm
|
|
|
216
216
|
return registry;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export { buildSearchReport,
|
|
219
|
+
export { buildSearchReport, searchCandidatesForCli };
|
|
220
220
|
|
|
221
221
|
export async function dispatchCli(args: string[], options: DispatchCliOptions = {}): Promise<CliDispatchResult> {
|
|
222
222
|
let parsed;
|
package/src/cli.test.ts
CHANGED
|
@@ -6,14 +6,11 @@ import { AppPersistence } from "./data/app-persistence";
|
|
|
6
6
|
import { loadConfig, saveConfig } from "./data/config/store";
|
|
7
7
|
import {
|
|
8
8
|
buildSearchReport,
|
|
9
|
-
buildTickerReport,
|
|
10
9
|
runCli,
|
|
11
10
|
searchCandidatesForCli,
|
|
12
11
|
} from "./cli/index";
|
|
13
12
|
import { createDefaultConfig } from "./types/config";
|
|
14
13
|
import { TickerRepository } from "./data/ticker-repository";
|
|
15
|
-
import type { TickerFinancials } from "./types/financials";
|
|
16
|
-
import type { NewsItem, SecFilingItem } from "./types/data-provider";
|
|
17
14
|
import type { TickerRecord } from "./types/ticker";
|
|
18
15
|
import { createTestDataProvider } from "./test-support/data-provider";
|
|
19
16
|
|
|
@@ -375,160 +372,6 @@ describe("CLI portfolio commands", () => {
|
|
|
375
372
|
});
|
|
376
373
|
});
|
|
377
374
|
|
|
378
|
-
describe("buildTickerReport", () => {
|
|
379
|
-
test("includes deeper fundamentals and recent statement sections", async () => {
|
|
380
|
-
const config = createDefaultConfig("/tmp/gloomberb-cli-report");
|
|
381
|
-
config.watchlists = [{ id: "growth", name: "Growth" }];
|
|
382
|
-
|
|
383
|
-
const ticker = makeTicker({
|
|
384
|
-
portfolios: ["main"],
|
|
385
|
-
watchlists: ["growth"],
|
|
386
|
-
positions: [{
|
|
387
|
-
portfolio: "main",
|
|
388
|
-
shares: 10,
|
|
389
|
-
avgCost: 400,
|
|
390
|
-
broker: "manual",
|
|
391
|
-
}],
|
|
392
|
-
assetCategory: "STK",
|
|
393
|
-
sector: "Technology",
|
|
394
|
-
industry: "Semiconductors",
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const financials: TickerFinancials = {
|
|
398
|
-
quote: {
|
|
399
|
-
symbol: "NVDA",
|
|
400
|
-
price: 912.34,
|
|
401
|
-
currency: "USD",
|
|
402
|
-
change: 12.5,
|
|
403
|
-
changePercent: 1.39,
|
|
404
|
-
lastUpdated: Date.UTC(2026, 3, 1, 14, 30),
|
|
405
|
-
marketCap: 2_240_000_000_000,
|
|
406
|
-
volume: 48_000_000,
|
|
407
|
-
name: "NVIDIA Corporation",
|
|
408
|
-
exchangeName: "NMS",
|
|
409
|
-
marketState: "REGULAR",
|
|
410
|
-
bid: 912.3,
|
|
411
|
-
ask: 912.4,
|
|
412
|
-
bidSize: 100,
|
|
413
|
-
askSize: 200,
|
|
414
|
-
open: 905,
|
|
415
|
-
high: 918,
|
|
416
|
-
low: 901,
|
|
417
|
-
high52w: 980,
|
|
418
|
-
low52w: 500,
|
|
419
|
-
dataSource: "live",
|
|
420
|
-
},
|
|
421
|
-
fundamentals: {
|
|
422
|
-
trailingPE: 35.2,
|
|
423
|
-
forwardPE: 29.8,
|
|
424
|
-
pegRatio: 1.4,
|
|
425
|
-
enterpriseValue: 2_300_000_000_000,
|
|
426
|
-
operatingCashFlow: 82_000_000_000,
|
|
427
|
-
freeCashFlow: 67_000_000_000,
|
|
428
|
-
dividendYield: 0.001,
|
|
429
|
-
revenue: 128_000_000_000,
|
|
430
|
-
netIncome: 73_000_000_000,
|
|
431
|
-
eps: 2.97,
|
|
432
|
-
operatingMargin: 0.58,
|
|
433
|
-
profitMargin: 0.57,
|
|
434
|
-
revenueGrowth: 0.42,
|
|
435
|
-
return1Y: 0.88,
|
|
436
|
-
return3Y: 2.45,
|
|
437
|
-
lastQuarterGrowth: 0.61,
|
|
438
|
-
sharesOutstanding: 2_450_000_000,
|
|
439
|
-
},
|
|
440
|
-
profile: {
|
|
441
|
-
description: "Designs GPUs and accelerated computing platforms.",
|
|
442
|
-
sector: "Technology",
|
|
443
|
-
industry: "Semiconductors",
|
|
444
|
-
},
|
|
445
|
-
annualStatements: [{
|
|
446
|
-
date: "2025-12-31",
|
|
447
|
-
totalRevenue: 128_000_000_000,
|
|
448
|
-
grossProfit: 97_000_000_000,
|
|
449
|
-
operatingIncome: 76_000_000_000,
|
|
450
|
-
netIncome: 73_000_000_000,
|
|
451
|
-
ebitda: 79_000_000_000,
|
|
452
|
-
operatingCashFlow: 82_000_000_000,
|
|
453
|
-
freeCashFlow: 67_000_000_000,
|
|
454
|
-
cashAndCashEquivalents: 40_000_000_000,
|
|
455
|
-
totalAssets: 110_000_000_000,
|
|
456
|
-
totalLiabilities: 36_000_000_000,
|
|
457
|
-
totalDebt: 9_000_000_000,
|
|
458
|
-
totalEquity: 74_000_000_000,
|
|
459
|
-
eps: 11.84,
|
|
460
|
-
dilutedShares: 2_450_000_000,
|
|
461
|
-
}],
|
|
462
|
-
quarterlyStatements: [{
|
|
463
|
-
date: "2026-03-31",
|
|
464
|
-
totalRevenue: 38_000_000_000,
|
|
465
|
-
grossProfit: 28_000_000_000,
|
|
466
|
-
operatingIncome: 22_000_000_000,
|
|
467
|
-
netIncome: 21_000_000_000,
|
|
468
|
-
ebitda: 23_000_000_000,
|
|
469
|
-
operatingCashFlow: 24_000_000_000,
|
|
470
|
-
freeCashFlow: 19_000_000_000,
|
|
471
|
-
cashAndCashEquivalents: 42_000_000_000,
|
|
472
|
-
totalAssets: 118_000_000_000,
|
|
473
|
-
totalLiabilities: 38_000_000_000,
|
|
474
|
-
totalDebt: 9_000_000_000,
|
|
475
|
-
totalEquity: 80_000_000_000,
|
|
476
|
-
eps: 3.4,
|
|
477
|
-
dilutedShares: 2_455_000_000,
|
|
478
|
-
}],
|
|
479
|
-
priceHistory: [],
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
const recentNews: NewsItem[] = [{
|
|
483
|
-
title: "NVIDIA unveils next platform",
|
|
484
|
-
source: "Example News",
|
|
485
|
-
url: "https://example.com/nvda-platform",
|
|
486
|
-
publishedAt: "2026-04-01T15:45:00.000Z" as unknown as Date,
|
|
487
|
-
summary: "Analysts expect the launch to expand datacenter demand.",
|
|
488
|
-
}];
|
|
489
|
-
|
|
490
|
-
const recentSecFilings: SecFilingItem[] = [{
|
|
491
|
-
accessionNumber: "0000000000-26-000001",
|
|
492
|
-
form: "8-K",
|
|
493
|
-
filingDate: "2026-03-31T00:00:00.000Z" as unknown as Date,
|
|
494
|
-
cik: "0001045810",
|
|
495
|
-
filingUrl: "https://www.sec.gov/Archives/example-8k",
|
|
496
|
-
primaryDocument: "nvda-8k.htm",
|
|
497
|
-
primaryDocDescription: "Current report announcing a product launch",
|
|
498
|
-
items: "2.02, 7.01",
|
|
499
|
-
}];
|
|
500
|
-
|
|
501
|
-
const report = await buildTickerReport({
|
|
502
|
-
symbol: "NVDA",
|
|
503
|
-
tickerFile: ticker,
|
|
504
|
-
financials,
|
|
505
|
-
config,
|
|
506
|
-
toBase: async (value) => value,
|
|
507
|
-
notes: "Conviction remains high.\nWatch gross margin guidance.",
|
|
508
|
-
recentNews,
|
|
509
|
-
recentSecFilings,
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
expect(report).toContain("Fundamentals");
|
|
513
|
-
expect(report).toContain("(+1.39%)");
|
|
514
|
-
expect(report).not.toContain("++1.39%");
|
|
515
|
-
expect(report).toContain("Operating Cash Flow");
|
|
516
|
-
expect(report).toContain("Latest Annual (2025-12-31)");
|
|
517
|
-
expect(report).toContain("Latest Quarter (2026-03-31)");
|
|
518
|
-
expect(report).toContain("Watchlists Growth");
|
|
519
|
-
expect(report).toContain("Designs GPUs and accelerated computing platforms.");
|
|
520
|
-
expect(report).toContain("Notes");
|
|
521
|
-
expect(report).toContain("Conviction remains high.");
|
|
522
|
-
expect(report).toContain("Recent News");
|
|
523
|
-
expect(report).toContain("NVIDIA unveils next platform");
|
|
524
|
-
expect(report).toContain("Example News");
|
|
525
|
-
expect(report).toContain("Apr 1, 2026");
|
|
526
|
-
expect(report).toContain("Recent SEC Filings");
|
|
527
|
-
expect(report).toContain("8-K | Mar 31, 2026");
|
|
528
|
-
expect(report).toContain("Current report announcing a product launch");
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
375
|
describe("CLI search helpers", () => {
|
|
533
376
|
test("merges local ticker matches with provider company search results", async () => {
|
|
534
377
|
const candidates = await searchCandidatesForCli({
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
renderNativeBarChart,
|
|
8
8
|
resolveNativeBarPixelBounds,
|
|
9
9
|
resolveBarChartHover,
|
|
10
|
+
resolveNativeBarChartHover,
|
|
10
11
|
} from "./bar-chart-renderer";
|
|
11
12
|
|
|
12
13
|
const series = [
|
|
@@ -122,16 +123,14 @@ describe("bar chart renderer", () => {
|
|
|
122
123
|
colors: { bgColor: "#000000", gridColor: "#333333", axisColor: "#666666", negativeColor: "#ff5555" },
|
|
123
124
|
})!;
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
126
|
+
const zeroRow = renderBarChart(scene)[scene.zeroRow]!;
|
|
127
|
+
expect([...zeroRow].filter((char) => char === "█").length).toBeGreaterThan(0);
|
|
128
|
+
expect([...zeroRow].filter((char) => char === "─").length).toBeGreaterThanOrEqual(scene.categories.length - 1);
|
|
128
129
|
|
|
129
130
|
const bitmapWidth = 470;
|
|
130
131
|
const pixelBounds = scene.bars.map((bar) => resolveNativeBarPixelBounds(scene, bar, bitmapWidth));
|
|
131
132
|
const pixelWidths = pixelBounds.map((bounds) => bounds.rightExclusive - bounds.left);
|
|
132
133
|
expect(Math.max(...pixelWidths) - Math.min(...pixelWidths)).toBeLessThanOrEqual(1);
|
|
133
|
-
expect(pixelBounds[0]!.left).toBe(0);
|
|
134
|
-
expect(pixelBounds.at(-1)!.rightExclusive).toBe(bitmapWidth);
|
|
135
134
|
const pixelGaps: number[] = [];
|
|
136
135
|
for (let index = 1; index < pixelBounds.length; index++) {
|
|
137
136
|
pixelGaps.push(pixelBounds[index]!.left - pixelBounds[index - 1]!.rightExclusive);
|
|
@@ -139,13 +138,7 @@ describe("bar chart renderer", () => {
|
|
|
139
138
|
expect(new Set(pixelGaps)).toEqual(new Set([1]));
|
|
140
139
|
|
|
141
140
|
const bitmap = renderNativeBarChart(scene, bitmapWidth, 80);
|
|
142
|
-
|
|
143
|
-
let separatorColumns = 0;
|
|
144
|
-
for (let x = 0; x < bitmap.width; x++) {
|
|
145
|
-
const index = (zeroY * bitmap.width + x) * 4;
|
|
146
|
-
if (bitmap.pixels[index + 1]! <= 120) separatorColumns++;
|
|
147
|
-
}
|
|
148
|
-
expect(separatorColumns).toBe(scene.categories.length - 1);
|
|
141
|
+
expect(bitmap.pixels.some((value) => value > 0)).toBe(true);
|
|
149
142
|
|
|
150
143
|
const axis = renderBarChartAxis(scene, 1);
|
|
151
144
|
expect(axis).toHaveLength(1);
|
|
@@ -155,4 +148,86 @@ describe("bar chart renderer", () => {
|
|
|
155
148
|
expect(axis.join("\n")).not.toContain("20 20");
|
|
156
149
|
expect(axis[0]!.endsWith(" ")).toBe(true);
|
|
157
150
|
});
|
|
151
|
+
|
|
152
|
+
test("drops null-only categories while keeping fixed ticker slots", () => {
|
|
153
|
+
const sparseSeries = [
|
|
154
|
+
{
|
|
155
|
+
id: "AMD",
|
|
156
|
+
label: "AMD",
|
|
157
|
+
color: "#00ff66",
|
|
158
|
+
points: [
|
|
159
|
+
{ category: "2025 Q1", value: 10 },
|
|
160
|
+
{ category: "2025 Q2", value: 11 },
|
|
161
|
+
{ category: "2025 Q3", value: 12 },
|
|
162
|
+
{ category: "2025 Q4", value: null },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "NVDA",
|
|
167
|
+
label: "NVDA",
|
|
168
|
+
color: "#4dabf7",
|
|
169
|
+
points: [
|
|
170
|
+
{ category: "2025 Q1", value: 20 },
|
|
171
|
+
{ category: "2025 Q3", value: 30 },
|
|
172
|
+
{ category: "2025 Q4", value: null },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
const scene = buildBarChartScene(sparseSeries, {
|
|
177
|
+
width: 36,
|
|
178
|
+
height: 8,
|
|
179
|
+
colors: { bgColor: "#000000", gridColor: "#333333", axisColor: "#666666", negativeColor: "#ff5555" },
|
|
180
|
+
})!;
|
|
181
|
+
|
|
182
|
+
expect(scene.categories).toEqual(["2025 Q1", "2025 Q2", "2025 Q3"]);
|
|
183
|
+
const middleCategoryBars = scene.bars.filter((bar) => bar.category === "2025 Q2");
|
|
184
|
+
expect(middleCategoryBars).toHaveLength(1);
|
|
185
|
+
expect(middleCategoryBars[0]!.seriesCount).toBe(2);
|
|
186
|
+
|
|
187
|
+
const bitmapWidth = 360;
|
|
188
|
+
const firstAmd = resolveNativeBarPixelBounds(scene, scene.bars.find((bar) => bar.category === "2025 Q1" && bar.seriesId === "AMD")!, bitmapWidth);
|
|
189
|
+
const middleAmd = resolveNativeBarPixelBounds(scene, middleCategoryBars[0]!, bitmapWidth);
|
|
190
|
+
expect(middleAmd.rightExclusive - middleAmd.left).toBe(firstAmd.rightExclusive - firstAmd.left);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("spreads saturated text slots and resolves native hover by pixel slot", () => {
|
|
194
|
+
const denseMultiSeries = Array.from({ length: 3 }, (_unused, seriesIndex) => ({
|
|
195
|
+
id: `S${seriesIndex}`,
|
|
196
|
+
label: `S${seriesIndex}`,
|
|
197
|
+
color: "#00ff66",
|
|
198
|
+
points: Array.from({ length: 80 }, (_point, categoryIndex) => ({
|
|
199
|
+
category: `${2020 + Math.floor(categoryIndex / 4)} Q${categoryIndex % 4 + 1}`,
|
|
200
|
+
value: seriesIndex + categoryIndex + 1,
|
|
201
|
+
})),
|
|
202
|
+
}));
|
|
203
|
+
const scene = buildBarChartScene(denseMultiSeries, {
|
|
204
|
+
width: 80,
|
|
205
|
+
height: 8,
|
|
206
|
+
colors: { bgColor: "#000000", gridColor: "#333333", axisColor: "#666666", negativeColor: "#ff5555" },
|
|
207
|
+
})!;
|
|
208
|
+
|
|
209
|
+
const xPositions = scene.bars.map((bar) => bar.x);
|
|
210
|
+
expect(Math.min(...xPositions)).toBe(0);
|
|
211
|
+
expect(Math.max(...xPositions)).toBe(scene.width - 1);
|
|
212
|
+
expect(new Set(xPositions).size).toBe(scene.width);
|
|
213
|
+
expect(xPositions.filter((x) => x === scene.width - 1).length).toBeLessThanOrEqual(denseMultiSeries.length);
|
|
214
|
+
|
|
215
|
+
const target = scene.bars.find((bar, index) => (
|
|
216
|
+
scene.bars.findIndex((candidate) => candidate.x === bar.x) < index
|
|
217
|
+
))!;
|
|
218
|
+
const bitmapWidth = 1200;
|
|
219
|
+
const bounds = resolveNativeBarPixelBounds(scene, target, bitmapWidth);
|
|
220
|
+
const pixelCenter = Math.floor((bounds.left + bounds.rightExclusive - 1) / 2);
|
|
221
|
+
|
|
222
|
+
expect(scene.bars.filter((bar) => bar.x === target.x).length).toBeGreaterThan(1);
|
|
223
|
+
expect(resolveBarChartHover(scene, target.x)).not.toMatchObject({
|
|
224
|
+
seriesId: target.seriesId,
|
|
225
|
+
category: target.category,
|
|
226
|
+
});
|
|
227
|
+
expect(resolveNativeBarChartHover(scene, pixelCenter, bitmapWidth)).toMatchObject({
|
|
228
|
+
seriesId: target.seriesId,
|
|
229
|
+
category: target.category,
|
|
230
|
+
value: target.value,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
158
233
|
});
|