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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { buildRelationshipAnalysis, createRelationshipPaneTemplate } from "../correlation/relationship/pane";
|
|
3
3
|
import { buildFundamentalGraphRows, buildValuationGraphRows } from "./data-panes/fundamental-graph";
|
|
4
|
+
import { allMetricDefs, buildGraphBarSeries, graphRowsForFinancials } from "./data-panes/fundamental-graph/model";
|
|
4
5
|
import { buildHistoricalPriceRows } from "./data-panes/historical-prices";
|
|
5
6
|
import { createDefaultConfig } from "../../../types/config";
|
|
6
|
-
import type { FinancialStatement, PricePoint } from "../../../types/financials";
|
|
7
|
+
import type { FinancialStatement, PricePoint, TickerFinancials } from "../../../types/financials";
|
|
7
8
|
|
|
8
9
|
describe("ticker data panes", () => {
|
|
9
10
|
test("builds historical price rows with close-to-close changes", () => {
|
|
@@ -65,6 +66,55 @@ describe("ticker data panes", () => {
|
|
|
65
66
|
]);
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
test("orders multi-symbol chart categories by statement date", () => {
|
|
70
|
+
const series = buildGraphBarSeries([
|
|
71
|
+
{ key: "NVDA:2025-01-31", symbol: "NVDA", date: "2025-01-31", category: "2025 Q1", value: 1, growth: null, barWidth: 1 },
|
|
72
|
+
{ key: "NVDA:2026-04-30", symbol: "NVDA", date: "2026-04-30", category: "2026 Q2", value: 2, growth: null, barWidth: 1 },
|
|
73
|
+
{ key: "AMD:2024-12-31", symbol: "AMD", date: "2024-12-31", category: "2024 Q4", value: 3, growth: null, barWidth: 1 },
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
expect(series[0]?.points.map((point) => point.category)).toEqual(["2024 Q4", "2025 Q1", "2026 Q2"]);
|
|
77
|
+
expect(series[1]?.points.map((point) => point.category)).toEqual(["2024 Q4", "2025 Q1", "2026 Q2"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("derives free cash flow and margins for graph metrics", () => {
|
|
81
|
+
const fcfRows = buildFundamentalGraphRows([
|
|
82
|
+
{ date: "2025-03-31", operatingCashFlow: 100, capitalExpenditure: -25 },
|
|
83
|
+
{ date: "2025-06-30", freeCashFlow: 90, operatingCashFlow: 120, capitalExpenditure: -30 },
|
|
84
|
+
] satisfies FinancialStatement[], "freeCashFlow", "NVDA", "quarterly");
|
|
85
|
+
const marginRows = buildFundamentalGraphRows([
|
|
86
|
+
{ date: "2025-03-31", totalRevenue: 200, grossProfit: 100 },
|
|
87
|
+
] satisfies FinancialStatement[], "grossMargin", "AMD", "quarterly");
|
|
88
|
+
|
|
89
|
+
expect(fcfRows.map((row) => row.value)).toEqual([75, 90]);
|
|
90
|
+
expect(marginRows[0]?.value).toBe(0.5);
|
|
91
|
+
expect(allMetricDefs().map(({ definition }) => definition.key)).toContain("freeCashFlowMargin");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("fills missing Q4 margin rows from annual totals and preceding quarters", () => {
|
|
95
|
+
const financials = {
|
|
96
|
+
annualStatements: [
|
|
97
|
+
{ date: "2025-12-31", totalRevenue: 1_000, grossProfit: 500 },
|
|
98
|
+
],
|
|
99
|
+
quarterlyStatements: [
|
|
100
|
+
{ date: "2025-03-31", totalRevenue: 100, grossProfit: 40 },
|
|
101
|
+
{ date: "2025-06-30", totalRevenue: 200, grossProfit: 90 },
|
|
102
|
+
{ date: "2025-09-30", totalRevenue: 300, grossProfit: 150 },
|
|
103
|
+
{ date: "2025-12-31" },
|
|
104
|
+
],
|
|
105
|
+
priceHistory: [],
|
|
106
|
+
} satisfies TickerFinancials;
|
|
107
|
+
|
|
108
|
+
const rows = graphRowsForFinancials(financials, "fundamental", "grossMargin", "quarterly", "AMD");
|
|
109
|
+
|
|
110
|
+
expect(rows.map((row) => [row.category, row.value])).toEqual([
|
|
111
|
+
["2025 Q1", 0.4],
|
|
112
|
+
["2025 Q2", 0.45],
|
|
113
|
+
["2025 Q3", 0.5],
|
|
114
|
+
["2025 Q4", 220 / 400],
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
68
118
|
test("builds valuation graph rows from available multiples", () => {
|
|
69
119
|
const rows = buildValuationGraphRows({
|
|
70
120
|
quote: { symbol: "AMD", price: 100, currency: "USD", change: 0, changePercent: 0, lastUpdated: 1, marketCap: 1_000 },
|
|
@@ -83,6 +133,50 @@ describe("ticker data panes", () => {
|
|
|
83
133
|
]);
|
|
84
134
|
});
|
|
85
135
|
|
|
136
|
+
test("builds historical trailing and forward P/E rows from prices and EPS", () => {
|
|
137
|
+
const financials = {
|
|
138
|
+
quote: { symbol: "META", price: 320, currency: "USD", change: 0, changePercent: 0, lastUpdated: 1 },
|
|
139
|
+
fundamentals: { forwardPE: 16 },
|
|
140
|
+
annualStatements: [
|
|
141
|
+
{ date: "2024-12-31", eps: 5 },
|
|
142
|
+
{ date: "2025-12-31", eps: 10 },
|
|
143
|
+
{ date: "2026-12-31", eps: 20 },
|
|
144
|
+
],
|
|
145
|
+
quarterlyStatements: [
|
|
146
|
+
{ date: "2025-03-31", eps: 1 },
|
|
147
|
+
{ date: "2025-06-30", eps: 2 },
|
|
148
|
+
{ date: "2025-09-30", eps: 3 },
|
|
149
|
+
{ date: "2025-12-31", eps: 4 },
|
|
150
|
+
{ date: "2026-03-31", eps: 5 },
|
|
151
|
+
],
|
|
152
|
+
priceHistory: [
|
|
153
|
+
{ date: new Date("2024-12-31T00:00:00Z"), close: 100 },
|
|
154
|
+
{ date: new Date("2025-12-31T00:00:00Z"), close: 150 },
|
|
155
|
+
{ date: new Date("2026-03-31T00:00:00Z"), close: 180 },
|
|
156
|
+
{ date: new Date("2026-12-31T00:00:00Z"), close: 300 },
|
|
157
|
+
],
|
|
158
|
+
} satisfies TickerFinancials;
|
|
159
|
+
|
|
160
|
+
const trailingAnnual = buildValuationGraphRows(financials, "trailingPE", "META", "annual");
|
|
161
|
+
const forwardAnnual = buildValuationGraphRows(financials, "forwardPE", "META", "annual");
|
|
162
|
+
const trailingQuarterly = buildValuationGraphRows(financials, "trailingPE", "META", "quarterly");
|
|
163
|
+
|
|
164
|
+
expect(trailingAnnual.map((row) => [row.category, row.value])).toEqual([
|
|
165
|
+
["FY2024", 20],
|
|
166
|
+
["FY2025", 15],
|
|
167
|
+
["FY2026", 15],
|
|
168
|
+
]);
|
|
169
|
+
expect(forwardAnnual.map((row) => [row.category, row.value])).toEqual([
|
|
170
|
+
["FY2024", 10],
|
|
171
|
+
["FY2025", 7.5],
|
|
172
|
+
["Current", 16],
|
|
173
|
+
]);
|
|
174
|
+
expect(trailingQuarterly.map((row) => [row.category, row.value])).toEqual([
|
|
175
|
+
["2025 Q4", 15],
|
|
176
|
+
["2026 Q1", 180 / 14],
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
|
|
86
180
|
test("builds relationship analysis from overlapping price history", () => {
|
|
87
181
|
const dates = ["2026-01-01", "2026-01-02", "2026-01-03", "2026-01-04", "2026-01-05", "2026-01-06"];
|
|
88
182
|
const makeHistory = (closes: number[]) => dates.map((date, index) => ({
|
|
@@ -274,13 +274,6 @@ export function ResolvedFinancialsTab({
|
|
|
274
274
|
})),
|
|
275
275
|
];
|
|
276
276
|
const rows = buildFinancialRows(subTab.rows, displayStatements, collapsedGroups);
|
|
277
|
-
const selectedIndex = selectedRowId
|
|
278
|
-
? rows.findIndex((row) => row.id === selectedRowId)
|
|
279
|
-
: rows.length > 0 ? 0 : -1;
|
|
280
|
-
const effectiveSelectedIndex = selectedIndex >= 0
|
|
281
|
-
? selectedIndex
|
|
282
|
-
: rows.length > 0 ? 0 : -1;
|
|
283
|
-
|
|
284
277
|
useEffect(() => {
|
|
285
278
|
if (rows.length === 0) {
|
|
286
279
|
if (selectedRowId !== null) setSelectedRowId(null);
|
|
@@ -5,6 +5,7 @@ import { normalizeTickerInput } from "../../../tickers/search";
|
|
|
5
5
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
6
6
|
import { TickerChartPane, ChartResearchTab } from "./chart-pane";
|
|
7
7
|
import {
|
|
8
|
+
buildGraphPaneSettingsDef,
|
|
8
9
|
createGraphPaneTemplate,
|
|
9
10
|
FundamentalGraphPane,
|
|
10
11
|
FundamentalGraphsResearchTab,
|
|
@@ -128,6 +129,7 @@ export const tickerDetailPlugin: GloomPlugin = {
|
|
|
128
129
|
defaultPosition: "right",
|
|
129
130
|
defaultMode: "floating",
|
|
130
131
|
defaultFloatingSize: { width: 82, height: 22 },
|
|
132
|
+
settings: buildGraphPaneSettingsDef(),
|
|
131
133
|
},
|
|
132
134
|
{
|
|
133
135
|
id: "provider-search-results",
|
|
@@ -13,10 +13,12 @@ import type { PinTickerOptions } from "../../../../types/plugin";
|
|
|
13
13
|
|
|
14
14
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
15
15
|
|
|
16
|
-
afterEach(() => {
|
|
16
|
+
afterEach(async () => {
|
|
17
17
|
setSharedMarketDataCoordinator(null);
|
|
18
18
|
if (testSetup) {
|
|
19
|
-
|
|
19
|
+
await act(async () => {
|
|
20
|
+
testSetup!.renderer.destroy();
|
|
21
|
+
});
|
|
20
22
|
testSetup = undefined;
|
|
21
23
|
}
|
|
22
24
|
});
|
|
@@ -181,44 +183,13 @@ async function renderOnce() {
|
|
|
181
183
|
async function flushFrames(count: number) {
|
|
182
184
|
for (let index = 0; index < count; index += 1) {
|
|
183
185
|
await act(async () => {
|
|
184
|
-
await Promise
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
185
187
|
await testSetup!.renderOnce();
|
|
186
188
|
});
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
describe("QuoteMonitorPane", () => {
|
|
191
|
-
test("renders a compact quote card for the bound ticker", async () => {
|
|
192
|
-
await renderHarness(createQuoteMonitorHarness(), {
|
|
193
|
-
width: 72,
|
|
194
|
-
height: 7,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await renderOnce();
|
|
198
|
-
|
|
199
|
-
const frame = testSetup.captureCharFrame();
|
|
200
|
-
expect(frame).toContain("MSFT");
|
|
201
|
-
expect(frame).toContain("$356.15");
|
|
202
|
-
expect(frame).toContain("-2.68%");
|
|
203
|
-
expect(frame).toContain("-9.82");
|
|
204
|
-
expect(frame).toMatch(/[⠁-⣿]/);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("renders multiple configured tickers", async () => {
|
|
208
|
-
await renderHarness(createQuoteMonitorHarness({ symbols: ["MSFT", "AAPL"] }), {
|
|
209
|
-
width: 72,
|
|
210
|
-
height: 8,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await renderOnce();
|
|
214
|
-
|
|
215
|
-
const frame = testSetup.captureCharFrame();
|
|
216
|
-
expect(frame).toContain("MSFT");
|
|
217
|
-
expect(frame).toContain("AAPL");
|
|
218
|
-
expect(frame).toContain("$356.15");
|
|
219
|
-
expect(frame).toContain("$202.12");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
193
|
test("opens a Ticker Research pane on the second card click", async () => {
|
|
223
194
|
const pinCalls: PinTickerCall[] = [];
|
|
224
195
|
await renderHarness(createQuoteMonitorHarness({ pinCalls }), {
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
buildFlatRows,
|
|
10
10
|
DEFAULT_SORT_PREFERENCE,
|
|
11
11
|
nextSortPreference,
|
|
12
|
-
rowIndicesOf,
|
|
13
12
|
type IndexQuoteState,
|
|
14
13
|
type QuoteMap,
|
|
15
14
|
type WorldIndexSortPreference,
|
|
@@ -36,12 +35,9 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
|
36
35
|
() => buildFlatRows(indicesByRegion, sortPreference, quotes),
|
|
37
36
|
[indicesByRegion, quotes, sortPreference],
|
|
38
37
|
);
|
|
39
|
-
const navigableIndices = useMemo(() => rowIndicesOf(flatRows), [flatRows]);
|
|
40
38
|
const selectedFlatIdx = selectedSymbol
|
|
41
39
|
? flatRows.findIndex((row) => row.type === "row" && row.entry.symbol === selectedSymbol)
|
|
42
40
|
: -1;
|
|
43
|
-
const activeFlatIdx = selectedFlatIdx >= 0 ? selectedFlatIdx : (navigableIndices[0] ?? -1);
|
|
44
|
-
|
|
45
41
|
useEffect(() => {
|
|
46
42
|
if (selectedSymbol && selectedFlatIdx >= 0) return;
|
|
47
43
|
const firstRow = flatRows.find((row) => row.type === "row");
|
|
@@ -87,13 +87,6 @@ export function buildFlatRows(
|
|
|
87
87
|
return rows;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function rowIndicesOf(flatRows: WorldIndexTableRow[]): number[] {
|
|
91
|
-
return flatRows.reduce<number[]>((acc, row, i) => {
|
|
92
|
-
if (row.type === "row") acc.push(i);
|
|
93
|
-
return acc;
|
|
94
|
-
}, []);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
90
|
export function nextSortPreference(
|
|
98
91
|
current: WorldIndexSortPreference,
|
|
99
92
|
columnId: string,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BrokerInstanceConfig } from "../../types/config";
|
|
2
2
|
import type { CachePolicy } from "../../types/persistence";
|
|
3
3
|
import { normalizeIbkrConfig } from "./config";
|
|
4
|
-
import { fnv1aHashString } from "
|
|
4
|
+
import { fnv1aHashString } from "../../utils/hash";
|
|
5
5
|
|
|
6
6
|
const FLEX_ACCOUNT_CACHE_POLICY = {
|
|
7
7
|
staleMs: 6 * 60 * 60 * 1000,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "@stoqey/ib";
|
|
7
7
|
import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
8
8
|
import type { Quote } from "../../../types/financials";
|
|
9
|
-
import {
|
|
9
|
+
import { normalizeSymbol } from "../../../utils/exchanges";
|
|
10
10
|
import type { IbkrGatewayConfig } from "./types";
|
|
11
11
|
import { getIbkrPriceDivisor, normalizeIbkrPriceValue } from "./price-normalization";
|
|
12
12
|
|
|
@@ -35,7 +35,7 @@ export function normalizeQuoteStreamTarget(target: QuoteSubscriptionTarget): Quo
|
|
|
35
35
|
return {
|
|
36
36
|
...target,
|
|
37
37
|
symbol,
|
|
38
|
-
exchange:
|
|
38
|
+
exchange: target.exchange?.trim().toUpperCase() ?? "",
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -136,15 +136,22 @@ export function applyTickByTickAllLastToQuote(
|
|
|
136
136
|
const nextPrice = normalizeIbkrPriceValue(tick.price, priceDivisor);
|
|
137
137
|
if (nextPrice == null || nextPrice <= 0) return null;
|
|
138
138
|
|
|
139
|
-
const
|
|
140
|
-
|
|
139
|
+
const canInferPreviousClose = current?.price != null
|
|
140
|
+
&& current.change != null
|
|
141
|
+
&& (current.change !== 0 || current.changePercent !== 0);
|
|
142
|
+
const inferredPreviousClose = current?.previousClose
|
|
143
|
+
?? (canInferPreviousClose ? current.price - current.change : undefined);
|
|
144
|
+
const previousClose = inferredPreviousClose != null && Number.isFinite(inferredPreviousClose) && inferredPreviousClose > 0
|
|
145
|
+
? inferredPreviousClose
|
|
146
|
+
: undefined;
|
|
147
|
+
const change = previousClose != null ? nextPrice - previousClose : current?.change ?? 0;
|
|
141
148
|
return {
|
|
142
149
|
symbol: current?.symbol ?? contract.localSymbol ?? contract.symbol ?? "",
|
|
143
150
|
providerId: "ibkr",
|
|
144
151
|
price: nextPrice,
|
|
145
152
|
currency: current?.currency ?? contract.currency ?? "USD",
|
|
146
153
|
change,
|
|
147
|
-
changePercent: previousClose ? (change / previousClose) * 100 : 0,
|
|
154
|
+
changePercent: previousClose ? (change / previousClose) * 100 : current?.changePercent ?? 0,
|
|
148
155
|
previousClose,
|
|
149
156
|
high52w: current?.high52w,
|
|
150
157
|
low52w: current?.low52w,
|
|
@@ -312,6 +312,37 @@ describe("tick-by-tick quote updates", () => {
|
|
|
312
312
|
expect(next?.lastUpdated).toBe(1_700_000_000_000);
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
test("does not use the previous tick as the daily close when no close is known", () => {
|
|
316
|
+
const current = {
|
|
317
|
+
symbol: "AAPL",
|
|
318
|
+
providerId: "ibkr" as const,
|
|
319
|
+
price: 249,
|
|
320
|
+
currency: "USD",
|
|
321
|
+
change: 0,
|
|
322
|
+
changePercent: 0,
|
|
323
|
+
name: "Apple Inc.",
|
|
324
|
+
lastUpdated: 1000,
|
|
325
|
+
dataSource: "live" as const,
|
|
326
|
+
};
|
|
327
|
+
const tick: TickByTickAllLast = {
|
|
328
|
+
time: 1_700_000_000,
|
|
329
|
+
price: 250.5,
|
|
330
|
+
size: 100,
|
|
331
|
+
tickType: 1,
|
|
332
|
+
tickAttribLast: {},
|
|
333
|
+
exchange: "NASDAQ",
|
|
334
|
+
specialConditions: "",
|
|
335
|
+
contract: contract as any,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const next = applyTickByTickAllLastToQuote(current, contract as any, details, tick, 1, "live");
|
|
339
|
+
|
|
340
|
+
expect(next?.price).toBe(250.5);
|
|
341
|
+
expect(next?.previousClose).toBeUndefined();
|
|
342
|
+
expect(next?.change).toBe(0);
|
|
343
|
+
expect(next?.changePercent).toBe(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
315
346
|
test("applies bid/ask ticks without disturbing the current trade price", () => {
|
|
316
347
|
const current = {
|
|
317
348
|
symbol: "AAPL",
|
|
@@ -5,7 +5,7 @@ import type { BrokerPortfolioPerformance } from "../../types/trading";
|
|
|
5
5
|
import { debugLog } from "../../utils/debug-log";
|
|
6
6
|
import { loadFlexStatement, parseFlexPortfolioPerformance } from "./flex";
|
|
7
7
|
import { normalizeIbkrConfig } from "./config";
|
|
8
|
-
import { fnv1aHashString } from "
|
|
8
|
+
import { fnv1aHashString } from "../../utils/hash";
|
|
9
9
|
|
|
10
10
|
const PERFORMANCE_LOG = debugLog.createLogger("ibkr-performance");
|
|
11
11
|
const PERFORMANCE_CACHE_KIND = "portfolio-performance";
|
|
@@ -5,6 +5,7 @@ import type { TickerRecord } from "../../../types/ticker";
|
|
|
5
5
|
import type { BrokerAccount, BrokerOrderPreview, BrokerOrderType } from "../../../types/trading";
|
|
6
6
|
import { colors } from "../../../theme/colors";
|
|
7
7
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
8
|
+
export { truncateWithEllipsis as truncateTradeText } from "../../../utils/text-wrap";
|
|
8
9
|
import { formatMarketPrice, formatMarketPriceWithCurrency, formatSignedMarketPrice, type AssetDisplayContext } from "../../../market-data/market/format";
|
|
9
10
|
import { getConfiguredIbkrGatewayInstances } from "../instance-selection";
|
|
10
11
|
|
|
@@ -108,13 +109,6 @@ export function formatPreviewMetric(before?: number, after?: number): string {
|
|
|
108
109
|
return `${formatCompact(before || 0)} → ${formatCompact(after || 0)}`;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
export function truncateTradeText(value: string, maxWidth: number): string {
|
|
112
|
-
if (maxWidth <= 0) return "";
|
|
113
|
-
if (value.length <= maxWidth) return value;
|
|
114
|
-
if (maxWidth <= 3) return value.slice(0, maxWidth);
|
|
115
|
-
return `${value.slice(0, maxWidth - 3)}...`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
112
|
export function findTickerForOrder(
|
|
119
113
|
order: { contract: BrokerContractRef },
|
|
120
114
|
tickers: Map<string, TickerRecord>,
|
package/src/plugins/loader.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readdir } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
4
5
|
import type { GloomPlugin } from "../types/plugin";
|
|
5
6
|
import { debugLog } from "../utils/debug-log";
|
|
6
7
|
|
|
7
8
|
const loaderLog = debugLog.createLogger("plugin-loader");
|
|
8
9
|
|
|
9
|
-
const PLUGINS_DIR = join(process.env.HOME ||
|
|
10
|
+
const PLUGINS_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugins");
|
|
10
11
|
|
|
11
12
|
export interface LoadedExternalPlugin {
|
|
12
13
|
plugin: GloomPlugin;
|
|
@@ -64,38 +64,4 @@ describe("prediction markets detail views", () => {
|
|
|
64
64
|
expect(frame).not.toContain("Loading market detail...");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test("renders book levels in the shared data table", async () => {
|
|
68
|
-
testSetup = await testRender(<GroupedDetailHarness detailTab="book" />, {
|
|
69
|
-
width: 64,
|
|
70
|
-
height: 24,
|
|
71
|
-
});
|
|
72
|
-
await flushFrames(testSetup);
|
|
73
|
-
|
|
74
|
-
const frame = testSetup.captureCharFrame();
|
|
75
|
-
expect(frame).toContain("OUT");
|
|
76
|
-
expect(frame).toContain("SIDE");
|
|
77
|
-
expect(frame).toContain("PRICE");
|
|
78
|
-
expect(frame).toContain("SIZE");
|
|
79
|
-
expect(frame).toContain("YES");
|
|
80
|
-
expect(frame).toContain("BID");
|
|
81
|
-
expect(frame).toContain("47c");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("renders recent trades in the shared data table", async () => {
|
|
85
|
-
testSetup = await testRender(<GroupedDetailHarness detailTab="trades" />, {
|
|
86
|
-
width: 64,
|
|
87
|
-
height: 24,
|
|
88
|
-
});
|
|
89
|
-
await flushFrames(testSetup);
|
|
90
|
-
|
|
91
|
-
const frame = testSetup.captureCharFrame();
|
|
92
|
-
expect(frame).toContain("TIME");
|
|
93
|
-
expect(frame).toContain("SIDE");
|
|
94
|
-
expect(frame).toContain("OUT");
|
|
95
|
-
expect(frame).toContain("PRICE");
|
|
96
|
-
expect(frame).toContain("SIZE");
|
|
97
|
-
expect(frame).toContain("BUY");
|
|
98
|
-
expect(frame).toContain("YES");
|
|
99
|
-
expect(frame).toContain("48c");
|
|
100
|
-
});
|
|
101
67
|
});
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from "../../types/plugin";
|
|
15
15
|
import type { PaneRuntimeState } from "../../core/state/app/state";
|
|
16
16
|
import type { NewsQuery, NewsQueryState } from "../../types/news-source";
|
|
17
|
+
import type { SyncContributor, SyncTransport } from "../../sync/types";
|
|
17
18
|
import { debugLog } from "../../utils/debug-log";
|
|
18
19
|
import { createPluginPersistence } from "../plugin-persistence";
|
|
19
20
|
import type { PluginEvents } from "../event-bus";
|
|
@@ -35,6 +36,8 @@ export interface RegistryPluginContextOptions {
|
|
|
35
36
|
updateLayout: (layout: LayoutConfig) => void;
|
|
36
37
|
resolvePaneTarget: (paneId: string) => string | undefined;
|
|
37
38
|
registerCapabilityForPlugin: (pluginId: string, capability: PluginCapability, items: PluginItems) => void;
|
|
39
|
+
registerSyncContributorForPlugin: (pluginId: string, contributor: SyncContributor) => () => void;
|
|
40
|
+
registerSyncTransportForPlugin: (pluginId: string, transport: SyncTransport) => () => void;
|
|
38
41
|
watchNewsQuery: (query: NewsQuery, listener: (state: NewsQueryState) => void) => () => void;
|
|
39
42
|
getData: (ticker: string) => TickerFinancials | null;
|
|
40
43
|
getTicker: (symbol: string) => TickerRecord | null;
|
|
@@ -82,6 +85,8 @@ export function createRegistryPluginContext({
|
|
|
82
85
|
updateLayout,
|
|
83
86
|
resolvePaneTarget,
|
|
84
87
|
registerCapabilityForPlugin,
|
|
88
|
+
registerSyncContributorForPlugin,
|
|
89
|
+
registerSyncTransportForPlugin,
|
|
85
90
|
watchNewsQuery,
|
|
86
91
|
getData,
|
|
87
92
|
getTicker,
|
|
@@ -143,6 +148,16 @@ export function createRegistryPluginContext({
|
|
|
143
148
|
registerShortcut: (shortcut) => contributions.registerShortcut(pluginId, shortcut, items),
|
|
144
149
|
registerTickerAction: (action) => contributions.registerTickerAction(pluginId, action, items),
|
|
145
150
|
registerContextMenuProvider: (provider) => contributions.registerContextMenuProvider(pluginId, provider, items),
|
|
151
|
+
registerSyncContributor: (contributor) => {
|
|
152
|
+
const dispose = registerSyncContributorForPlugin(pluginId, contributor);
|
|
153
|
+
items.eventDisposers.push(dispose);
|
|
154
|
+
return dispose;
|
|
155
|
+
},
|
|
156
|
+
registerSyncTransport: (transport) => {
|
|
157
|
+
const dispose = registerSyncTransportForPlugin(pluginId, transport);
|
|
158
|
+
items.eventDisposers.push(dispose);
|
|
159
|
+
return dispose;
|
|
160
|
+
},
|
|
146
161
|
watchNewsQuery: (query, listener) => {
|
|
147
162
|
const dispose = watchNewsQuery(query, listener);
|
|
148
163
|
items.newsQueryWatchDisposers.push(dispose);
|
|
@@ -37,6 +37,11 @@ interface RegistryContributionsOptions {
|
|
|
37
37
|
wrapBrokerAdapter?: (broker: BrokerAdapter, pluginId: string) => BrokerAdapter;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function setUnique<T>(map: Map<string, T>, id: string, value: T): void {
|
|
41
|
+
if (map.has(id)) throw new Error(`Duplicate plugin contribution id: ${id}`);
|
|
42
|
+
map.set(id, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
export class RegistryContributions {
|
|
41
46
|
readonly pluginItems = new Map<string, PluginItems>();
|
|
42
47
|
readonly commandOwners = new Map<string, string>();
|
|
@@ -82,30 +87,30 @@ export class RegistryContributions {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
registerPane(pluginId: string, pane: PaneDef, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
85
|
-
this.panesMap
|
|
90
|
+
setUnique(this.panesMap, pane.id, this.options.wrapPaneDef(pluginId, pane));
|
|
86
91
|
this.paneOwners.set(pane.id, pluginId);
|
|
87
92
|
items.panes.push(pane.id);
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
registerPaneTemplate(pluginId: string, template: PaneTemplateDef, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
91
|
-
this.paneTemplatesMap
|
|
96
|
+
setUnique(this.paneTemplatesMap, template.id, template);
|
|
92
97
|
this.paneTemplateOwners.set(template.id, pluginId);
|
|
93
98
|
items.paneTemplates.push(template.id);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
registerCommand(pluginId: string, command: CommandDef, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
97
|
-
this.commandsMap
|
|
102
|
+
setUnique(this.commandsMap, command.id, command);
|
|
98
103
|
this.commandOwners.set(command.id, pluginId);
|
|
99
104
|
items.commands.push(command.id);
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
registerColumn(_pluginId: string, column: CustomColumnDef, items: PluginItems): void {
|
|
103
|
-
this.columnsMap
|
|
108
|
+
setUnique(this.columnsMap, column.id, column);
|
|
104
109
|
items.columns.push(column.id);
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
registerBroker(pluginId: string, broker: BrokerAdapter, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
108
|
-
this.brokersMap
|
|
113
|
+
setUnique(this.brokersMap, broker.id, this.options.wrapBrokerAdapter?.(broker, pluginId) ?? broker);
|
|
109
114
|
items.brokers.push(broker.id);
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -115,19 +120,19 @@ export class RegistryContributions {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
registerTickerResearchTab(pluginId: string, tab: TickerResearchTabDef, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
118
|
-
this.tickerResearchTabsMap
|
|
123
|
+
setUnique(this.tickerResearchTabsMap, tab.id, this.options.wrapTickerResearchTabDef(pluginId, tab));
|
|
119
124
|
this.tickerResearchTabOwners.set(tab.id, pluginId);
|
|
120
125
|
items.tickerResearchTabs.push(tab.id);
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
registerShortcut(pluginId: string, shortcut: KeyboardShortcut, items = this.getOrCreatePluginItems(pluginId)): void {
|
|
124
|
-
this.shortcutsMap
|
|
129
|
+
setUnique(this.shortcutsMap, shortcut.id, shortcut);
|
|
125
130
|
this.shortcutOwners.set(shortcut.id, pluginId);
|
|
126
131
|
items.shortcuts.push(shortcut.id);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
registerTickerAction(_pluginId: string, action: TickerAction, items: PluginItems): void {
|
|
130
|
-
this.tickerActionsMap
|
|
135
|
+
setUnique(this.tickerActionsMap, action.id, action);
|
|
131
136
|
items.tickerActions.push(action.id);
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -166,9 +171,15 @@ export class RegistryContributions {
|
|
|
166
171
|
}
|
|
167
172
|
for (const actionId of items.tickerActions) this.tickerActionsMap.delete(actionId);
|
|
168
173
|
for (const providerKey of items.contextMenuProviders) this.contextMenuProvidersMap.delete(providerKey);
|
|
169
|
-
|
|
170
|
-
for (const dispose of items.capabilityDisposers)
|
|
171
|
-
|
|
174
|
+
let disposeError: unknown;
|
|
175
|
+
for (const dispose of [...items.eventDisposers, ...items.capabilityDisposers, ...items.newsQueryWatchDisposers]) {
|
|
176
|
+
try {
|
|
177
|
+
dispose();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
disposeError ??= error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
172
182
|
this.pluginItems.delete(pluginId);
|
|
183
|
+
if (disposeError) throw disposeError;
|
|
173
184
|
}
|
|
174
185
|
}
|
|
@@ -70,6 +70,49 @@ afterEach(() => {
|
|
|
70
70
|
currentPersistence = null;
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
describe("PluginRegistry lifecycle", () => {
|
|
74
|
+
test("rolls back rejected registrations and always removes owned contributions", async () => {
|
|
75
|
+
const registry = createRegistry();
|
|
76
|
+
const pane = {
|
|
77
|
+
id: "shared-pane",
|
|
78
|
+
name: "Shared",
|
|
79
|
+
component: () => null,
|
|
80
|
+
defaultPosition: "right" as const,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await expect(registry.register({
|
|
84
|
+
id: "setup-failure",
|
|
85
|
+
name: "Setup failure",
|
|
86
|
+
version: "1.0.0",
|
|
87
|
+
panes: [pane],
|
|
88
|
+
setup: () => { throw new Error("setup failed"); },
|
|
89
|
+
})).rejects.toThrow("setup failed");
|
|
90
|
+
expect(registry.allPlugins.has("setup-failure")).toBe(false);
|
|
91
|
+
expect(registry.panes.has("shared-pane")).toBe(false);
|
|
92
|
+
|
|
93
|
+
await registry.register({ id: "owner", name: "Owner", version: "1.0.0", panes: [pane] });
|
|
94
|
+
await expect(registry.register({ id: "collision", name: "Collision", version: "1.0.0", panes: [pane] }))
|
|
95
|
+
.rejects.toThrow("Duplicate plugin contribution id");
|
|
96
|
+
expect(registry.panes.get("shared-pane")?.name).toBe("Shared");
|
|
97
|
+
|
|
98
|
+
await registry.register({
|
|
99
|
+
id: "throwing-dispose",
|
|
100
|
+
name: "Throwing dispose",
|
|
101
|
+
version: "1.0.0",
|
|
102
|
+
panes: [{
|
|
103
|
+
id: "disposable-pane",
|
|
104
|
+
name: "Disposable",
|
|
105
|
+
component: () => null,
|
|
106
|
+
defaultPosition: "right",
|
|
107
|
+
}],
|
|
108
|
+
dispose: () => { throw new Error("dispose failed"); },
|
|
109
|
+
});
|
|
110
|
+
expect(() => registry.unregister("throwing-dispose")).toThrow("dispose failed");
|
|
111
|
+
expect(registry.allPlugins.has("throwing-dispose")).toBe(false);
|
|
112
|
+
expect(registry.panes.has("disposable-pane")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
73
116
|
describe("PluginRegistry context menu providers", () => {
|
|
74
117
|
test("registers and unregisters context menu providers", async () => {
|
|
75
118
|
const registry = createRegistry();
|
|
@@ -234,6 +277,7 @@ describe("PluginRegistry pane settings", () => {
|
|
|
234
277
|
settings: {},
|
|
235
278
|
}],
|
|
236
279
|
floating: [],
|
|
280
|
+
detached: [],
|
|
237
281
|
};
|
|
238
282
|
registry.getConfigFn = () => config;
|
|
239
283
|
registry.getLayoutFn = () => config.layout;
|