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.
Files changed (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /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
- testSetup.renderer.destroy();
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.resolve();
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 "./hash";
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 { canonicalExchange, normalizeSymbol } from "../../../utils/exchanges";
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: canonicalExchange(target.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 previousClose = current?.previousClose ?? current?.price ?? nextPrice;
140
- const change = nextPrice - previousClose;
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 "./hash";
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>,
@@ -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 || "~", ".gloomberb", "plugins");
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.set(pane.id, this.options.wrapPaneDef(pluginId, pane));
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.set(template.id, template);
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.set(command.id, command);
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.set(column.id, column);
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.set(broker.id, this.options.wrapBrokerAdapter?.(broker, pluginId) ?? broker);
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.set(tab.id, this.options.wrapTickerResearchTabDef(pluginId, tab));
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.set(shortcut.id, shortcut);
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.set(action.id, action);
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
- for (const dispose of items.eventDisposers) dispose();
170
- for (const dispose of items.capabilityDisposers) dispose();
171
- for (const dispose of items.newsQueryWatchDisposers) dispose();
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;