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
@@ -27,6 +27,12 @@ import type {
27
27
  } from "../../types/plugin";
28
28
  import type { ContextMenuContext, ContextMenuItem } from "../../types/context-menu";
29
29
  import type { TickerRecord } from "../../types/ticker";
30
+ import type {
31
+ RegisteredSyncContributor,
32
+ RegisteredSyncTransport,
33
+ SyncContributor,
34
+ SyncTransport,
35
+ } from "../../sync/types";
30
36
  import { EventBus } from "../event-bus";
31
37
  import { resolvePaneInstance } from "../../types/config";
32
38
  import { debugLog } from "../../utils/debug-log";
@@ -49,6 +55,7 @@ import {
49
55
  releaseSharedRegistry,
50
56
  } from "./shared";
51
57
  import { RegistryResumeStateListeners } from "./plugin-state";
58
+ import { cloudSyncController } from "../../sync/controller";
52
59
 
53
60
  interface PluginRegistryOptions {
54
61
  enableCapabilityHandlers?: boolean;
@@ -215,6 +222,28 @@ export class PluginRegistry implements PluginRuntimeAccess {
215
222
  get tickerActions(): ReadonlyMap<string, TickerAction> { return this.contributions.tickerActionsMap; }
216
223
  get allPlugins(): ReadonlyMap<string, GloomPlugin> { return this.plugins; }
217
224
 
225
+ registerSyncContributorForPlugin(pluginId: string, contributor: SyncContributor): () => void {
226
+ return cloudSyncController.registerContributor(pluginId, contributor);
227
+ }
228
+
229
+ registerSyncTransportForPlugin(pluginId: string, transport: SyncTransport): () => void {
230
+ return cloudSyncController.registerTransport(pluginId, transport);
231
+ }
232
+
233
+ getEnabledSyncContributors(): RegisteredSyncContributor[] {
234
+ const disabledPlugins = new Set(this.getConfigFn().disabledPlugins ?? []);
235
+ return cloudSyncController
236
+ .getRegisteredContributors()
237
+ .filter((entry) => !disabledPlugins.has(entry.pluginId));
238
+ }
239
+
240
+ getActiveSyncTransport(): RegisteredSyncTransport | null {
241
+ const disabledPlugins = new Set(this.getConfigFn().disabledPlugins ?? []);
242
+ return cloudSyncController
243
+ .getRegisteredTransports()
244
+ .find((entry) => !disabledPlugins.has(entry.pluginId) && entry.transport.isAvailable()) ?? null;
245
+ }
246
+
218
247
  getContextMenuItems(context: ContextMenuContext): ContextMenuItem[] {
219
248
  return resolveRegistryContextMenuItems({
220
249
  context,
@@ -373,6 +402,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
373
402
  updateLayout: (layout) => this.updateLayoutFn(layout),
374
403
  resolvePaneTarget: (paneId) => this.resolvePaneTarget(paneId),
375
404
  registerCapabilityForPlugin: (targetPluginId, capability, pluginItems) => this.registerCapabilityForPlugin(targetPluginId, capability, pluginItems),
405
+ registerSyncContributorForPlugin: (targetPluginId, contributor) => this.registerSyncContributorForPlugin(targetPluginId, contributor),
406
+ registerSyncTransportForPlugin: (targetPluginId, transport) => this.registerSyncTransportForPlugin(targetPluginId, transport),
376
407
  watchNewsQuery: (query, listener) => this.watchNewsQueryFn(query, listener),
377
408
  getData: (ticker) => this.getDataFn(ticker),
378
409
  getTicker: (symbol) => this.getTickerFn(symbol),
@@ -410,35 +441,47 @@ export class PluginRegistry implements PluginRuntimeAccess {
410
441
 
411
442
  async register(plugin: GloomPlugin): Promise<void> {
412
443
  this.registryLog.info(`Registering plugin: ${plugin.id} v${plugin.version ?? "?"}`);
413
- const items = this.contributions.getOrCreatePluginItems(plugin.id);
414
- if (plugin.panes) {
415
- for (const pane of plugin.panes) {
416
- this.contributions.registerPane(plugin.id, pane, items);
444
+ if (this.plugins.has(plugin.id)) throw new Error(`Plugin already registered: ${plugin.id}`);
445
+ this.plugins.set(plugin.id, plugin);
446
+ try {
447
+ const items = this.contributions.getOrCreatePluginItems(plugin.id);
448
+ if (plugin.panes) {
449
+ for (const pane of plugin.panes) {
450
+ this.contributions.registerPane(plugin.id, pane, items);
451
+ }
417
452
  }
418
- }
419
453
 
420
- if (plugin.paneTemplates) {
421
- for (const template of plugin.paneTemplates) {
422
- this.contributions.registerPaneTemplate(plugin.id, template, items);
454
+ if (plugin.paneTemplates) {
455
+ for (const template of plugin.paneTemplates) {
456
+ this.contributions.registerPaneTemplate(plugin.id, template, items);
457
+ }
423
458
  }
424
- }
425
-
426
- if (plugin.broker) {
427
- this.contributions.registerBroker(plugin.id, plugin.broker, items);
428
- }
429
459
 
430
- if (this.enableCapabilityHandlers && plugin.capabilities) {
431
- for (const capability of plugin.capabilities) {
432
- this.registerCapabilityForPlugin(plugin.id, capability, items);
460
+ if (plugin.broker) {
461
+ this.contributions.registerBroker(plugin.id, plugin.broker, items);
433
462
  }
434
- }
435
463
 
436
- this.slots.register(plugin, this);
464
+ if (this.enableCapabilityHandlers && plugin.capabilities) {
465
+ for (const capability of plugin.capabilities) {
466
+ this.registerCapabilityForPlugin(plugin.id, capability, items);
467
+ }
468
+ }
437
469
 
438
- this.plugins.set(plugin.id, plugin);
470
+ this.slots.register(plugin, this);
439
471
 
440
- if (plugin.setup) {
441
- await plugin.setup(this.createContext(plugin.id));
472
+ if (plugin.setup) {
473
+ await plugin.setup(this.createContext(plugin.id));
474
+ }
475
+ } catch (error) {
476
+ try {
477
+ this.removePlugin(plugin.id);
478
+ } catch (cleanupError) {
479
+ this.registryLog.error("Failed to clean up rejected plugin registration", {
480
+ pluginId: plugin.id,
481
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
482
+ });
483
+ }
484
+ throw error;
442
485
  }
443
486
 
444
487
  this.events.emit("plugin:registered", { pluginId: plugin.id });
@@ -449,13 +492,19 @@ export class PluginRegistry implements PluginRuntimeAccess {
449
492
  if (!plugin) return;
450
493
  this.registryLog.info(`Unregistering plugin: ${pluginId}`);
451
494
 
452
- plugin.dispose?.();
453
- this.slots.unregister(pluginId);
454
-
455
- this.contributions.unregister(pluginId);
495
+ this.removePlugin(pluginId);
496
+ this.events.emit("plugin:unregistered", { pluginId });
497
+ }
456
498
 
499
+ private removePlugin(pluginId: string): void {
500
+ const plugin = this.plugins.get(pluginId);
501
+ if (!plugin) return;
502
+ let cleanupError: unknown;
503
+ try { plugin.dispose?.(); } catch (error) { cleanupError = error; }
504
+ try { this.slots.unregister(pluginId); } catch (error) { cleanupError ??= error; }
505
+ try { this.contributions.unregister(pluginId); } catch (error) { cleanupError ??= error; }
457
506
  this.plugins.delete(pluginId);
458
- this.events.emit("plugin:unregistered", { pluginId });
507
+ if (cleanupError) throw cleanupError;
459
508
  }
460
509
 
461
510
  destroy(): void {
@@ -51,7 +51,6 @@ interface AppRemoteControllerOptions {
51
51
  }
52
52
 
53
53
  const DEFAULT_MUTATION_INCLUDE: RemoteStateInclude[] = ["app", "layout", "panes", "commandBar"];
54
- const DEFAULT_BATCH_INCLUDE: RemoteStateInclude[] = ["app", "layout", "panes", "commandBar"];
55
54
 
56
55
  export function createAppRemoteController({
57
56
  dispatch,
@@ -68,11 +67,6 @@ export function createAppRemoteController({
68
67
  uiRegistry,
69
68
  });
70
69
 
71
- const getAfterMutation = async (resource: string): Promise<unknown> => {
72
- await afterMutation();
73
- return getResource(resource);
74
- };
75
-
76
70
  const getAfterMutationSummary = async (extra?: Record<string, unknown>): Promise<unknown> => {
77
71
  await afterMutation();
78
72
  return mutationSummary(getState(), extra);
@@ -420,7 +414,7 @@ export function createAppRemoteController({
420
414
  ok: responses.every((response) => response.ok),
421
415
  haltedAt,
422
416
  responses,
423
- }, undefined, buildIncludedState(request.include, request.dryRun ? [] : DEFAULT_BATCH_INCLUDE));
417
+ }, undefined, buildIncludedState(request.include, request.dryRun ? [] : DEFAULT_MUTATION_INCLUDE));
424
418
  }
425
419
  default:
426
420
  throw new Error(`Unknown remote request type: ${(request as { type?: unknown }).type}`);
@@ -12,6 +12,14 @@ function pointerSegments(path: string): string[] {
12
12
  return path.slice(1).split("/").map(decodePointerSegment);
13
13
  }
14
14
 
15
+ const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
16
+
17
+ function assertSafeObjectKey(key: string): void {
18
+ if (UNSAFE_OBJECT_KEYS.has(key)) {
19
+ throw new Error(`Unsafe object key in JSON patch path: ${key}`);
20
+ }
21
+ }
22
+
15
23
  function cloneJson<T>(value: T): T {
16
24
  return value == null ? value : JSON.parse(JSON.stringify(value)) as T;
17
25
  }
@@ -30,12 +38,18 @@ function containerFor(root: unknown, path: string): { parent: unknown; key: stri
30
38
  }
31
39
  parent = parent[index];
32
40
  } else if (parent && typeof parent === "object") {
41
+ assertSafeObjectKey(segment);
42
+ if (!Object.prototype.hasOwnProperty.call(parent, segment)) {
43
+ throw new Error(`Cannot descend into missing object key: ${segment}`);
44
+ }
33
45
  parent = (parent as Record<string, unknown>)[segment];
34
46
  } else {
35
47
  throw new Error(`Cannot descend into non-object patch path: ${path}`);
36
48
  }
37
49
  }
38
- return { parent, key: segments[segments.length - 1]! };
50
+ const key = segments[segments.length - 1]!;
51
+ if (!Array.isArray(parent)) assertSafeObjectKey(key);
52
+ return { parent, key };
39
53
  }
40
54
 
41
55
  function applyOperation(root: unknown, operation: RemoteJsonPatchOperation): void {
@@ -45,21 +45,31 @@ export async function startRemoteControlServer(options: RemoteControlServerOptio
45
45
  token,
46
46
  startedAt: new Date().toISOString(),
47
47
  };
48
- await writeRemoteEndpointFiles(options.dataDir, endpoint);
48
+ try {
49
+ await writeRemoteEndpointFiles(options.dataDir, endpoint);
50
+ } catch (error) {
51
+ await closeHttpServer(httpServer);
52
+ await removeRemoteEndpointFiles(options.dataDir, endpoint).catch(() => {});
53
+ throw error;
54
+ }
49
55
 
50
56
  return {
51
57
  endpoint,
52
58
  close: async () => {
53
- await new Promise<void>((resolve, reject) => {
54
- httpServer.close((error) => (error ? reject(error) : resolve()));
55
- }).catch((error) => {
56
- if ((error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING") throw error;
57
- });
59
+ await closeHttpServer(httpServer);
58
60
  await removeRemoteEndpointFiles(options.dataDir, endpoint);
59
61
  },
60
62
  };
61
63
  }
62
64
 
65
+ async function closeHttpServer(httpServer: ReturnType<typeof createServer>): Promise<void> {
66
+ await new Promise<void>((resolve, reject) => {
67
+ httpServer.close((error) => (error ? reject(error) : resolve()));
68
+ }).catch((error) => {
69
+ if ((error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING") throw error;
70
+ });
71
+ }
72
+
63
73
  async function handleHttpRequest(
64
74
  handle: RemoteControlServerOptions["handle"],
65
75
  token: string,
@@ -26,7 +26,7 @@ function openCommandBar(label: string, query: string, options?: { accelerator?:
26
26
  return commandItem(label, { type: "open-command-bar", query }, options);
27
27
  }
28
28
 
29
- export function buildApplicationMenu(): ApplicationMenuItemConfig[] {
29
+ function buildApplicationMenu(): ApplicationMenuItemConfig[] {
30
30
  return [
31
31
  {
32
32
  label: "Gloomberb",
@@ -19,6 +19,7 @@ const NOTES_INDEX_FILE = "__quick-notes-index__.json";
19
19
  const BROKER_INVOKE_OPERATIONS = new Set([
20
20
  "validate",
21
21
  "importPositions",
22
+ "importPortfolioSnapshot",
22
23
  "connect",
23
24
  "disconnect",
24
25
  "getPersistedConfigUpdate",
@@ -96,6 +97,7 @@ function invokeBrokerOperation(
96
97
  switch (operation) {
97
98
  case "validate":
98
99
  case "importPositions":
100
+ case "importPortfolioSnapshot":
99
101
  case "connect":
100
102
  case "disconnect":
101
103
  case "getPersistedConfigUpdate":
@@ -39,4 +39,18 @@ describe("handleHttpFetch", () => {
39
39
  expect(response.headers.location).toBe("/reader");
40
40
  expect(response.setCookie).toEqual(["substack.sid=sid123; Path=/; HttpOnly"]);
41
41
  });
42
+
43
+ test("aborts the backend fetch at the requested deadline", async () => {
44
+ server = Bun.serve({
45
+ port: 0,
46
+ fetch() {
47
+ return new Promise<Response>(() => {});
48
+ },
49
+ });
50
+
51
+ await expect(handleHttpFetch({
52
+ url: server.url.toString(),
53
+ init: { timeoutMs: 10 },
54
+ })).rejects.toThrow();
55
+ });
42
56
  });
@@ -34,12 +34,20 @@ export async function handleHttpFetch(payload: Record<string, unknown>) {
34
34
  typeof init.body === "string" && method !== "GET" && method !== "HEAD"
35
35
  ? init.body
36
36
  : undefined;
37
+ const timeoutMs =
38
+ typeof init.timeoutMs === "number"
39
+ && Number.isFinite(init.timeoutMs)
40
+ && init.timeoutMs > 0
41
+ && init.timeoutMs <= 120_000
42
+ ? init.timeoutMs
43
+ : undefined;
37
44
 
38
45
  const response = await fetch(url, {
39
46
  method,
40
47
  headers: normalizeHttpFetchHeaders(init.headers),
41
48
  body,
42
49
  redirect,
50
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,
43
51
  });
44
52
  const responseHeaders: Record<string, string> = {};
45
53
  response.headers.forEach((value, key) => {
@@ -1,4 +1,5 @@
1
1
  import { existsSync, mkdirSync } from "fs";
2
+ import { homedir } from "os";
2
3
  import { join } from "path";
3
4
  import { createAppServices, type AppServices } from "../../../../core/app-services";
4
5
  import type { AppSessionSnapshot } from "../../../../core/state/session-persistence";
@@ -98,7 +99,7 @@ function buildInitializationPayload(
98
99
  }
99
100
 
100
101
  async function resolveDesktopDataDir(): Promise<string> {
101
- const dataDir = await getDataDir() ?? join(process.env.HOME || "~", ".gloomberb");
102
+ const dataDir = await getDataDir() ?? join(process.env.HOME || homedir(), ".gloomberb");
102
103
  if (!existsSync(dataDir)) {
103
104
  mkdirSync(dataDir, { recursive: true });
104
105
  }
@@ -103,6 +103,41 @@ function requireDesktopWorkspace(): DesktopWorkspace {
103
103
  return desktopWorkspace;
104
104
  }
105
105
 
106
+ const pendingDesktopDeepLinks: string[] = [];
107
+
108
+ function isGloomberbDeepLink(rawUrl: string): boolean {
109
+ try {
110
+ return new URL(rawUrl).protocol === "gloomberb:";
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ function readOpenUrlEvent(event: unknown): string | null {
117
+ const data = event && typeof event === "object" ? (event as { data?: unknown }).data : null;
118
+ if (!data || typeof data !== "object") return null;
119
+ const url = (data as { url?: unknown }).url;
120
+ return typeof url === "string" && url ? url : null;
121
+ }
122
+
123
+ function sendDesktopDeepLink(rawUrl: string): void {
124
+ if (!isGloomberbDeepLink(rawUrl)) return;
125
+ const rpc = getWindowRpc(MAIN_WINDOW_RPC_KEY);
126
+ if (!rpc || !isWindowRpcReady(MAIN_WINDOW_RPC_KEY)) {
127
+ pendingDesktopDeepLinks.push(rawUrl);
128
+ if (pendingDesktopDeepLinks.length > 20) pendingDesktopDeepLinks.shift();
129
+ return;
130
+ }
131
+ detachedWindowManager.focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY);
132
+ rpc.send["desktop.deepLink"]({ url: rawUrl });
133
+ }
134
+
135
+ function flushPendingDesktopDeepLinks(): void {
136
+ if (pendingDesktopDeepLinks.length === 0) return;
137
+ const urls = pendingDesktopDeepLinks.splice(0);
138
+ for (const url of urls) sendDesktopDeepLink(url);
139
+ }
140
+
106
141
  function remoteFailure(code: string, message: string): RemoteControlResponse {
107
142
  return { ok: false, error: { code, message } };
108
143
  }
@@ -377,6 +412,7 @@ async function initialize(
377
412
  syncConfigAccessors,
378
413
  });
379
414
  if (init.windowKind === "main") {
415
+ flushPendingDesktopDeepLinks();
380
416
  void ensureDesktopRemoteControlServer().catch((error) => {
381
417
  console.error("[remote] desktop control endpoint failed", summarizeError(error));
382
418
  });
@@ -502,6 +538,12 @@ Electrobun.events.on("context-menu-clicked", (event: unknown) => {
502
538
  });
503
539
  });
504
540
 
541
+ Electrobun.events.on("open-url", (event: unknown) => {
542
+ const url = readOpenUrlEvent(event);
543
+ if (!url) return;
544
+ sendDesktopDeepLink(url);
545
+ });
546
+
505
547
  ApplicationMenu.on("application-menu-clicked", (event: unknown) => {
506
548
  const command = applicationMenuCommand(event);
507
549
  if (!command) return;
@@ -60,6 +60,10 @@ export interface DesktopRestartMessage {
60
60
  source?: string;
61
61
  }
62
62
 
63
+ export interface DesktopDeepLinkMessage {
64
+ url: string;
65
+ }
66
+
63
67
  export interface RemoteControlRequestMessage {
64
68
  request: RemoteControlRequest;
65
69
  }
@@ -89,6 +93,7 @@ export interface ElectrobunDesktopRpcSchema {
89
93
  "desktop.state": DesktopStateMessage;
90
94
  "desktop.dockPreview": DesktopDockPreviewMessage;
91
95
  "desktop.themePreview": DesktopThemePreviewMessage;
96
+ "desktop.deepLink": DesktopDeepLinkMessage;
92
97
  "update.progress": UpdateProgressMessage;
93
98
  "capability.event": CapabilityEventMessage;
94
99
  };
@@ -4,6 +4,7 @@ import {
4
4
  type ApplicationMenuSelectMessage,
5
5
  type CapabilityEventMessage,
6
6
  type ContextMenuSelectMessage,
7
+ type DesktopDeepLinkMessage,
7
8
  type DesktopDockPreviewMessage,
8
9
  type DesktopRestartMessage,
9
10
  type DesktopStateMessage,
@@ -18,6 +19,7 @@ import type { RemoteControlRequest, RemoteControlResponse } from "../../../remot
18
19
 
19
20
  type ContextMenuSelectListener = (message: ContextMenuSelectMessage) => void;
20
21
  type ApplicationMenuSelectListener = (message: ApplicationMenuSelectMessage) => void;
22
+ type DesktopDeepLinkListener = (message: DesktopDeepLinkMessage) => void;
21
23
  type DesktopStateListener = (message: DesktopStateMessage) => void;
22
24
  type DesktopDockPreviewListener = (message: DesktopDockPreviewMessage) => void;
23
25
  type DesktopThemePreviewListener = (message: DesktopThemePreviewMessage) => void;
@@ -29,6 +31,8 @@ let initSnapshot: ElectrobunBackendInit | null = null;
29
31
  let remoteControlRequestHandler: RemoteControlRequestHandler | null = null;
30
32
  const contextMenuSelectListeners = new Map<string, Set<ContextMenuSelectListener>>();
31
33
  const applicationMenuSelectListeners = new Set<ApplicationMenuSelectListener>();
34
+ const desktopDeepLinkListeners = new Set<DesktopDeepLinkListener>();
35
+ const pendingDesktopDeepLinks: DesktopDeepLinkMessage[] = [];
32
36
  const desktopStateListeners = new Set<DesktopStateListener>();
33
37
  const desktopDockPreviewListeners = new Set<DesktopDockPreviewListener>();
34
38
  const desktopThemePreviewListeners = new Set<DesktopThemePreviewListener>();
@@ -64,6 +68,17 @@ function subscribe<T>(
64
68
  };
65
69
  }
66
70
 
71
+ function dispatchDesktopDeepLink(message: DesktopDeepLinkMessage): void {
72
+ if (desktopDeepLinkListeners.size === 0) {
73
+ pendingDesktopDeepLinks.push(message);
74
+ if (pendingDesktopDeepLinks.length > 20) pendingDesktopDeepLinks.shift();
75
+ return;
76
+ }
77
+ for (const listener of desktopDeepLinkListeners) {
78
+ listener(message);
79
+ }
80
+ }
81
+
67
82
  const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
68
83
  maxRequestTime: 120_000,
69
84
  handlers: {
@@ -92,6 +107,10 @@ const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
92
107
  listener(decoded);
93
108
  }
94
109
  },
110
+ "desktop.deepLink": (message) => {
111
+ if (typeof message.url !== "string" || !message.url) return;
112
+ dispatchDesktopDeepLink({ url: message.url });
113
+ },
95
114
  "desktop.state": (message) => {
96
115
  for (const listener of desktopStateListeners) {
97
116
  listener({ snapshot: decodeRpcValue(message.snapshot) });
@@ -188,6 +207,16 @@ export function onApplicationMenuSelect(listener: ApplicationMenuSelectListener)
188
207
  };
189
208
  }
190
209
 
210
+ export function onDesktopDeepLink(listener: DesktopDeepLinkListener): () => void {
211
+ desktopDeepLinkListeners.add(listener);
212
+ for (const message of pendingDesktopDeepLinks.splice(0)) {
213
+ listener(message);
214
+ }
215
+ return () => {
216
+ desktopDeepLinkListeners.delete(listener);
217
+ };
218
+ }
219
+
191
220
  export function onDesktopState(listener: DesktopStateListener): () => void {
192
221
  desktopStateListeners.add(listener);
193
222
  return () => {
@@ -261,6 +261,7 @@ function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
261
261
  switchPanel: () => {},
262
262
  openCommandBar: () => {},
263
263
  showPane: () => {},
264
+ createPaneFromTemplate: () => {},
264
265
  hidePane: () => {},
265
266
  focusPane: () => {},
266
267
  openPaneSettings: () => {},
@@ -19,7 +19,6 @@ export const CSS_TEXT_DIM = "var(--gloom-text-dim)";
19
19
  export const CSS_TEXT_BRIGHT = "var(--gloom-text-bright)";
20
20
  export const CSS_SELECTED = "var(--gloom-selected)";
21
21
  export const CSS_SELECTED_TEXT = "var(--gloom-selected-text)";
22
- export const CSS_HOVER_BG = "var(--gloom-hover-bg)";
23
22
 
24
23
  function hasAttribute(attributes: unknown, flag: number): boolean {
25
24
  return typeof attributes === "number" && (attributes & flag) !== 0;