gloomberb 0.5.1 → 0.6.0

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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -14
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DEFAULT_WINDOW_FRAME,
4
+ MAIN_WINDOW_MIN_SIZE,
5
+ constrainWindowFrame,
6
+ normalizeWindowFrameWithMinimum,
7
+ } from "./window-frame";
8
+
9
+ describe("electrobun window frames", () => {
10
+ test("normalizes partial resize events without clamping usable small windows", () => {
11
+ const frame = normalizeWindowFrameWithMinimum(
12
+ { width: 720 },
13
+ { x: 10, y: 20, width: 1200, height: 800 },
14
+ MAIN_WINDOW_MIN_SIZE,
15
+ );
16
+
17
+ expect(frame).toEqual({
18
+ x: 10,
19
+ y: 20,
20
+ width: 720,
21
+ height: 800,
22
+ });
23
+ });
24
+
25
+ test("clamps only tiny frames to the emergency minimum size", () => {
26
+ expect(constrainWindowFrame(
27
+ { x: 0, y: 0, width: 400, height: 300 },
28
+ MAIN_WINDOW_MIN_SIZE,
29
+ )).toEqual({
30
+ x: 0,
31
+ y: 0,
32
+ width: 640,
33
+ height: 400,
34
+ });
35
+ });
36
+
37
+ test("uses the default frame when no frame data is available", () => {
38
+ expect(normalizeWindowFrameWithMinimum(null)).toEqual(DEFAULT_WINDOW_FRAME);
39
+ });
40
+ });
@@ -0,0 +1,46 @@
1
+ export interface WindowFrame {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export interface WindowMinimumSize {
9
+ width: number;
10
+ height: number;
11
+ }
12
+
13
+ export const DEFAULT_WINDOW_FRAME: WindowFrame = { x: 64, y: 48, width: 1440, height: 920 };
14
+ // Emergency fallback until Electrobun exposes stable native minimum-size handling:
15
+ // https://github.com/blackboardsh/electrobun/issues/188
16
+ export const MAIN_WINDOW_MIN_SIZE: WindowMinimumSize = { width: 640, height: 400 };
17
+ export const DETACHED_WINDOW_MIN_SIZE: WindowMinimumSize = { width: 360, height: 240 };
18
+
19
+ export function normalizeWindowFrame(
20
+ frame: Partial<WindowFrame> | null | undefined,
21
+ fallback: WindowFrame = DEFAULT_WINDOW_FRAME,
22
+ ): WindowFrame {
23
+ return {
24
+ x: typeof frame?.x === "number" ? frame.x : fallback.x,
25
+ y: typeof frame?.y === "number" ? frame.y : fallback.y,
26
+ width: typeof frame?.width === "number" ? frame.width : fallback.width,
27
+ height: typeof frame?.height === "number" ? frame.height : fallback.height,
28
+ };
29
+ }
30
+
31
+ export function constrainWindowFrame(frame: WindowFrame, minimumSize?: WindowMinimumSize): WindowFrame {
32
+ if (!minimumSize) return frame;
33
+ return {
34
+ ...frame,
35
+ width: Math.max(minimumSize.width, frame.width),
36
+ height: Math.max(minimumSize.height, frame.height),
37
+ };
38
+ }
39
+
40
+ export function normalizeWindowFrameWithMinimum(
41
+ frame: Partial<WindowFrame> | null | undefined,
42
+ fallback: WindowFrame = DEFAULT_WINDOW_FRAME,
43
+ minimumSize?: WindowMinimumSize,
44
+ ): WindowFrame {
45
+ return constrainWindowFrame(normalizeWindowFrame(frame, fallback), minimumSize);
46
+ }
@@ -0,0 +1,72 @@
1
+ import type { AppSessionSnapshot } from "../../../core/state/session-persistence";
2
+ import type { DesktopDockPreviewState, DesktopSharedStateSnapshot } from "../../../types/desktop-window";
3
+ import type { DesktopApplicationMenuCommand } from "../../../types/desktop-menu";
4
+ import type { AppConfig } from "../../../types/config";
5
+ import type { UpdateProgress } from "../../../updater";
6
+ import type { CapabilityManifest } from "../../../capabilities";
7
+
8
+ export const ELECTROBUN_CONTEXT_MENU_ACTION = "gloom.context-menu.select";
9
+
10
+ export interface ElectrobunBackendInit {
11
+ config: AppConfig;
12
+ sessionSnapshot: AppSessionSnapshot | null;
13
+ desktopSnapshot: DesktopSharedStateSnapshot | null;
14
+ pluginState: Record<string, Record<string, unknown>>;
15
+ capabilityManifests: CapabilityManifest[];
16
+ windowKind: "main" | "detached";
17
+ paneId?: string;
18
+ }
19
+
20
+ export interface BackendRequestPayload {
21
+ method: string;
22
+ payload?: unknown;
23
+ }
24
+
25
+ export interface ContextMenuSelectMessage {
26
+ requestId: string;
27
+ itemId: string;
28
+ }
29
+
30
+ export interface ApplicationMenuSelectMessage {
31
+ command: DesktopApplicationMenuCommand;
32
+ }
33
+
34
+ export interface DesktopStateMessage {
35
+ snapshot: DesktopSharedStateSnapshot;
36
+ }
37
+
38
+ export interface DesktopDockPreviewMessage {
39
+ preview: DesktopDockPreviewState;
40
+ }
41
+
42
+ export interface UpdateProgressMessage {
43
+ progress: UpdateProgress;
44
+ }
45
+
46
+ export interface CapabilityEventMessage {
47
+ subscriptionId: string;
48
+ event: unknown;
49
+ }
50
+
51
+ export interface ElectrobunDesktopRpcSchema {
52
+ bun: {
53
+ requests: {
54
+ "backend.request": {
55
+ params: BackendRequestPayload;
56
+ response: unknown;
57
+ };
58
+ };
59
+ messages: {};
60
+ };
61
+ webview: {
62
+ requests: {};
63
+ messages: {
64
+ "context-menu.select": ContextMenuSelectMessage;
65
+ "application-menu.select": ApplicationMenuSelectMessage;
66
+ "desktop.state": DesktopStateMessage;
67
+ "desktop.dockPreview": DesktopDockPreviewMessage;
68
+ "update.progress": UpdateProgressMessage;
69
+ "capability.event": CapabilityEventMessage;
70
+ };
71
+ };
72
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ AI_RUNNER_CAPABILITY_ID,
3
+ type AiRunnerEvent,
4
+ } from "../../../capabilities";
5
+ import {
6
+ getAiProviderDefinitions,
7
+ __setDetectedProvidersForTests,
8
+ } from "../../../plugins/builtin/ai/providers";
9
+ import { AiRunCancelledError, setAiRunHost } from "../../../plugins/builtin/ai/runner";
10
+ import { backendRequest, onCapabilityEvent } from "./backend-rpc";
11
+
12
+ let nextRunId = 1;
13
+
14
+ export async function installElectrobunAiHost(): Promise<void> {
15
+ const availability = await backendRequest<Record<string, boolean>>("capability.invoke", {
16
+ capabilityId: AI_RUNNER_CAPABILITY_ID,
17
+ operationId: "getProviderAvailability",
18
+ payload: {},
19
+ });
20
+ const providers = getAiProviderDefinitions().map((definition) => ({
21
+ ...definition,
22
+ available: availability[definition.id] ?? false,
23
+ }));
24
+
25
+ __setDetectedProvidersForTests(providers);
26
+ setAiRunHost({
27
+ run({ provider, prompt, cwd, onChunk }) {
28
+ const subscriptionId = `ai-run:${nextRunId++}`;
29
+ let disposed = false;
30
+ let settled = false;
31
+ let disposeMessages: () => void = () => {};
32
+ let resolveDone: (output: string) => void = () => {};
33
+ let rejectDone: (error: unknown) => void = () => {};
34
+
35
+ const cleanup = () => {
36
+ if (disposed) return;
37
+ disposed = true;
38
+ disposeMessages();
39
+ void backendRequest("capability.unsubscribe", { subscriptionId }).catch(() => {});
40
+ };
41
+
42
+ const settle = (callback: () => void) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ cleanup();
46
+ callback();
47
+ };
48
+
49
+ const done = new Promise<string>((resolve, reject) => {
50
+ resolveDone = resolve;
51
+ rejectDone = reject;
52
+ });
53
+
54
+ disposeMessages = onCapabilityEvent(subscriptionId, (message) => {
55
+ const event = message.event as AiRunnerEvent;
56
+ switch (event.kind) {
57
+ case "chunk":
58
+ onChunk?.(event.output);
59
+ break;
60
+ case "done":
61
+ settle(() => resolveDone(event.output));
62
+ break;
63
+ case "cancelled":
64
+ settle(() => rejectDone(new AiRunCancelledError()));
65
+ break;
66
+ case "error":
67
+ settle(() => rejectDone(new Error(event.error)));
68
+ break;
69
+ }
70
+ });
71
+
72
+ void backendRequest("capability.subscribe", {
73
+ subscriptionId,
74
+ capabilityId: AI_RUNNER_CAPABILITY_ID,
75
+ operationId: "run",
76
+ payload: {
77
+ providerId: provider.id,
78
+ prompt,
79
+ cwd,
80
+ },
81
+ }).catch((error) => {
82
+ settle(() => rejectDone(error));
83
+ });
84
+
85
+ return {
86
+ done,
87
+ cancel() {
88
+ settle(() => rejectDone(new AiRunCancelledError()));
89
+ },
90
+ };
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,348 @@
1
+ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
2
+ import { createRemoteBrokerAdapter } from "../../../brokers/remote-broker-adapter";
3
+ import { NewsService } from "../../../news/aggregator";
4
+ import { setSharedNewsService } from "../../../news/hooks";
5
+ import { PluginRegistry } from "../../../plugins/registry";
6
+ import type { AppServices } from "../../../core/app-services";
7
+ import type { AppConfig } from "../../../types/config";
8
+ import type { DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
9
+ import type { Quote } from "../../../types/financials";
10
+ import type { NewsArticle, NewsQuery } from "../../../news/types";
11
+ import type { TickerRecord, TickerMetadata } from "../../../types/ticker";
12
+ import { newsProvider, type CapabilityManifest } from "../../../capabilities";
13
+ import {
14
+ createPersistScheduler,
15
+ PLUGIN_STATE_SAVE_DEBOUNCE_MS,
16
+ SESSION_SAVE_DEBOUNCE_MS,
17
+ } from "../../../state/persist-scheduler";
18
+ import { backendRequest, getElectrobunBackendInitSnapshot, onCapabilityEvent } from "./backend-rpc";
19
+ import { DesktopMemoryResourceStore } from "./resource-store";
20
+ import { debugLog } from "../../../utils/debug-log";
21
+ import { measurePerf } from "../../../utils/perf-marks";
22
+ import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
23
+
24
+ const REMOTE_DATA_REQUEST_CACHE_LIMIT = 150;
25
+ const servicesLog = debugLog.createLogger("services");
26
+ const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
27
+ const NEWS_CAPABILITY_ID = "news.core";
28
+
29
+ class RemoteTickerRepository {
30
+ async loadAllTickers(): Promise<TickerRecord[]> {
31
+ return backendRequest<TickerRecord[]>("ticker.loadAll");
32
+ }
33
+
34
+ async loadTicker(symbol: string): Promise<TickerRecord | null> {
35
+ return backendRequest<TickerRecord | null>("ticker.load", { symbol });
36
+ }
37
+
38
+ async saveTicker(ticker: TickerRecord): Promise<void> {
39
+ await backendRequest("ticker.save", { ticker });
40
+ }
41
+
42
+ async createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
43
+ const ticker: TickerRecord = { metadata };
44
+ await this.saveTicker(ticker);
45
+ return ticker;
46
+ }
47
+
48
+ async deleteTicker(symbol: string): Promise<void> {
49
+ await backendRequest("ticker.delete", { symbol });
50
+ }
51
+ }
52
+
53
+ type RemoteAssetDataClient = DataProvider & {
54
+ getNews(query: NewsQuery): Promise<NewsArticle[]>;
55
+ };
56
+
57
+ type PayloadBuilder = (...args: any[]) => Record<string, unknown>;
58
+
59
+ const assetDataPayloads: Record<string, PayloadBuilder> = {
60
+ canProvide: (ticker, exchange, context) => ({ ticker, exchange, context }),
61
+ getTickerFinancials: (ticker, exchange, context) => ({ ticker, exchange, context }),
62
+ getQuote: (ticker, exchange, context) => ({ ticker, exchange, context }),
63
+ getExchangeRate: (fromCurrency) => ({ fromCurrency }),
64
+ search: (query, context) => ({ query, context }),
65
+ getSecFilings: (ticker, count, exchange, context) => ({ ticker, count, exchange, context }),
66
+ getHolders: (ticker, exchange, context) => ({ ticker, exchange, context }),
67
+ getAnalystResearch: (ticker, exchange, context) => ({ ticker, exchange, context }),
68
+ getCorporateActions: (ticker, exchange, context) => ({ ticker, exchange, context }),
69
+ getSecFilingContent: (filing) => ({ filing }),
70
+ getEarningsCalendar: (symbols, context) => ({ symbols, context }),
71
+ getArticleSummary: (url) => ({ url }),
72
+ getPriceHistory: (ticker, exchange, range, context) => ({ ticker, exchange, range, context }),
73
+ getPriceHistoryForResolution: (ticker, exchange, bufferRange, resolution, context) => ({ ticker, exchange, bufferRange, resolution, context }),
74
+ getDetailedPriceHistory: (ticker, exchange, startDate, endDate, barSize, context) => ({ ticker, exchange, startDate, endDate, barSize, context }),
75
+ getChartResolutionSupport: (ticker, exchange, context) => ({ ticker, exchange, context }),
76
+ getChartResolutionCapabilities: (ticker, exchange, context) => ({ ticker, exchange, context }),
77
+ getOptionsChain: (ticker, exchange, expirationDate, context) => ({ ticker, exchange, expirationDate, context }),
78
+ };
79
+
80
+ function findCapabilityManifest(capabilityId: string): CapabilityManifest | null {
81
+ return getElectrobunBackendInitSnapshot()?.capabilityManifests.find((manifest) => manifest.id === capabilityId) ?? null;
82
+ }
83
+
84
+ function getRendererOperationIds(capabilityId: string): Set<string> | null {
85
+ const manifest = findCapabilityManifest(capabilityId);
86
+ if (!manifest) return null;
87
+ return new Set(
88
+ manifest.operations
89
+ .filter((operation) => operation.rendererSafe)
90
+ .map((operation) => operation.id),
91
+ );
92
+ }
93
+
94
+ function hasRendererOperation(operations: Set<string> | null, operationId: string): boolean {
95
+ return operations === null || operations.has(operationId);
96
+ }
97
+
98
+ function createCapabilityInvoker() {
99
+ const requestCache = new Map<string, Promise<unknown>>();
100
+ return function invoke<T>(capabilityId: string, operationId: string, payload: unknown): Promise<T> {
101
+ const requestPayload = { capabilityId, operationId, payload };
102
+ const key = JSON.stringify(requestPayload);
103
+ const cached = requestCache.get(key);
104
+ if (cached) return cached as Promise<T>;
105
+ const promise = backendRequest<T>("capability.invoke", requestPayload).catch((error) => {
106
+ requestCache.delete(key);
107
+ throw error;
108
+ });
109
+ requestCache.set(key, promise);
110
+ if (requestCache.size > REMOTE_DATA_REQUEST_CACHE_LIMIT) {
111
+ const oldestKey = requestCache.keys().next().value;
112
+ if (oldestKey) requestCache.delete(oldestKey);
113
+ }
114
+ return promise;
115
+ };
116
+ }
117
+
118
+ function createRemoteAssetDataClient(): RemoteAssetDataClient {
119
+ const invoke = createCapabilityInvoker();
120
+ const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID);
121
+ const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID);
122
+ let nextSubscriptionId = 1;
123
+ const base = {
124
+ id: "desktop-backend",
125
+ name: "Gloomberb Backend",
126
+ getCachedFinancialsForTargets: () => new Map(),
127
+ getNews: (query: NewsQuery) => (
128
+ hasRendererOperation(newsOperations, "fetchNews")
129
+ ? invoke<NewsArticle[]>(NEWS_CAPABILITY_ID, "fetchNews", { query })
130
+ : Promise.resolve([])
131
+ ),
132
+ subscribeQuotes: (
133
+ targets: QuoteSubscriptionTarget[],
134
+ onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
135
+ ) => {
136
+ if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return () => {};
137
+ const uniqueTargets = [...new Map(
138
+ targets.map((target) => [
139
+ `${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}`,
140
+ target,
141
+ ] as const),
142
+ ).values()];
143
+ if (uniqueTargets.length === 0) return () => {};
144
+
145
+ const subscriptionId = `quote:${nextSubscriptionId++}`;
146
+ const disposeMessages = onCapabilityEvent(subscriptionId, (message) => {
147
+ const event = message.event as { target: QuoteSubscriptionTarget; quote: Quote };
148
+ onQuote(event.target, event.quote);
149
+ });
150
+ let disposed = false;
151
+
152
+ void backendRequest("capability.subscribe", {
153
+ subscriptionId,
154
+ capabilityId: ASSET_DATA_CAPABILITY_ID,
155
+ operationId: "subscribeQuotes",
156
+ payload: { targets: uniqueTargets },
157
+ }).catch((error) => {
158
+ disposeMessages();
159
+ console.error("Failed to subscribe to backend quotes", error);
160
+ });
161
+
162
+ return () => {
163
+ if (disposed) return;
164
+ disposed = true;
165
+ disposeMessages();
166
+ void backendRequest("capability.unsubscribe", { subscriptionId }).catch(() => {});
167
+ };
168
+ },
169
+ };
170
+
171
+ return new Proxy(base, {
172
+ get(target, prop, receiver) {
173
+ if (typeof prop !== "string" || prop in target) {
174
+ return Reflect.get(target, prop, receiver);
175
+ }
176
+ const buildPayload = assetDataPayloads[prop];
177
+ if (!buildPayload || !hasRendererOperation(assetDataOperations, prop)) return undefined;
178
+ return (...args: unknown[]) => invoke(ASSET_DATA_CAPABILITY_ID, prop, buildPayload(...args));
179
+ },
180
+ }) as RemoteAssetDataClient;
181
+ }
182
+
183
+ class RemoteSessionStore {
184
+ private snapshot = getElectrobunBackendInitSnapshot()?.sessionSnapshot ?? null;
185
+ private readonly scheduler = createPersistScheduler<{
186
+ sessionId: string;
187
+ value: unknown;
188
+ schemaVersion: number;
189
+ }>({
190
+ delayMs: SESSION_SAVE_DEBOUNCE_MS,
191
+ save: ({ sessionId, value, schemaVersion }) => backendRequest("session.set", { sessionId, value, schemaVersion }),
192
+ });
193
+
194
+ get<T>(sessionId = "app", schemaVersion = 1) {
195
+ if (sessionId !== "app" || !this.snapshot) return null;
196
+ return {
197
+ sessionId,
198
+ value: this.snapshot as T,
199
+ schemaVersion,
200
+ updatedAt: Date.now(),
201
+ };
202
+ }
203
+
204
+ set(sessionId: string, value: unknown, schemaVersion = 1): void {
205
+ if (sessionId === "app") this.snapshot = value as typeof this.snapshot;
206
+ this.scheduler.schedule({ sessionId, value, schemaVersion });
207
+ }
208
+
209
+ delete(sessionId: string): void {
210
+ if (sessionId === "app") this.snapshot = null;
211
+ this.scheduler.cancel();
212
+ void backendRequest("session.delete", { sessionId }).catch(() => {});
213
+ }
214
+
215
+ flush(): Promise<void> {
216
+ return this.scheduler.flush();
217
+ }
218
+ }
219
+
220
+ class RemotePluginStateStore {
221
+ private readonly state = new Map<string, Map<string, unknown>>();
222
+ private readonly schedulers = new Map<string, ReturnType<typeof createPersistScheduler<{
223
+ pluginId: string;
224
+ key: string;
225
+ value: unknown;
226
+ schemaVersion: number;
227
+ }>>>();
228
+
229
+ constructor(initial: Record<string, Record<string, unknown>>) {
230
+ for (const [pluginId, values] of Object.entries(initial)) {
231
+ this.state.set(pluginId, new Map(Object.entries(values)));
232
+ }
233
+ }
234
+
235
+ get<T>(pluginId: string, key: string, schemaVersion = 1) {
236
+ const value = this.state.get(pluginId)?.get(key);
237
+ if (value == null) return null;
238
+ return { value: value as T, schemaVersion, updatedAt: Date.now() };
239
+ }
240
+
241
+ set(pluginId: string, key: string, value: unknown, schemaVersion = 1): void {
242
+ if (!this.state.has(pluginId)) this.state.set(pluginId, new Map());
243
+ this.state.get(pluginId)!.set(key, value);
244
+ this.getScheduler(pluginId, key).schedule({ pluginId, key, value, schemaVersion });
245
+ }
246
+
247
+ delete(pluginId: string, key: string): void {
248
+ this.state.get(pluginId)?.delete(key);
249
+ this.getScheduler(pluginId, key).cancel();
250
+ void backendRequest("pluginState.delete", { pluginId, key }).catch(() => {});
251
+ }
252
+
253
+ keys(pluginId: string): string[] {
254
+ return [...(this.state.get(pluginId)?.keys() ?? [])];
255
+ }
256
+
257
+ clear(pluginId: string): void {
258
+ this.state.delete(pluginId);
259
+ }
260
+
261
+ async flush(): Promise<void> {
262
+ await Promise.all([...this.schedulers.values()].map((scheduler) => scheduler.flush()));
263
+ }
264
+
265
+ private getScheduler(pluginId: string, key: string) {
266
+ const schedulerKey = `${pluginId}\u0000${key}`;
267
+ let scheduler = this.schedulers.get(schedulerKey);
268
+ if (!scheduler) {
269
+ scheduler = createPersistScheduler({
270
+ delayMs: PLUGIN_STATE_SAVE_DEBOUNCE_MS,
271
+ save: (entry) => backendRequest("pluginState.set", entry),
272
+ });
273
+ this.schedulers.set(schedulerKey, scheduler);
274
+ }
275
+ return scheduler;
276
+ }
277
+ }
278
+
279
+ class RemotePersistence {
280
+ readonly tickers = {};
281
+ readonly resources = new DesktopMemoryResourceStore();
282
+ readonly pluginState = new RemotePluginStateStore(getElectrobunBackendInitSnapshot()?.pluginState ?? {});
283
+ readonly sessions = new RemoteSessionStore();
284
+ close(): void {
285
+ void this.sessions.flush();
286
+ void this.pluginState.flush();
287
+ }
288
+ }
289
+
290
+ export function createAppServices({ config }: { config: AppConfig }): AppServices {
291
+ servicesLog.info("create desktop web services start", {
292
+ brokerInstanceCount: config.brokerInstances.length,
293
+ });
294
+ const persistence = measurePerf("startup.services.persistence", () => new RemotePersistence());
295
+ const tickerRepository = measurePerf("startup.services.ticker-repository", () => new RemoteTickerRepository());
296
+ const dataProvider = measurePerf("startup.services.data-provider", () => createRemoteAssetDataClient());
297
+ const marketData = new MarketDataCoordinator(dataProvider);
298
+ const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository as never, persistence as never, {
299
+ enableCapabilityHandlers: false,
300
+ wrapBrokerAdapter: (broker) => createRemoteBrokerAdapter(broker),
301
+ });
302
+ const newsService = new NewsService();
303
+
304
+ pluginRegistry.getConfigFn = () => config;
305
+ pluginRegistry.getLayoutFn = () => config.layout;
306
+ pluginRegistry.registerNewsCapabilityFn = () => () => {};
307
+ pluginRegistry.watchNewsQueryFn = (query, listener) => newsService.watchQuery(query, listener);
308
+
309
+ setSharedMarketDataCoordinator(marketData);
310
+ setSharedNewsService(newsService);
311
+
312
+ newsService.register(newsProvider({
313
+ id: dataProvider.id,
314
+ name: dataProvider.name,
315
+ priority: 0,
316
+ provider: {
317
+ fetchNews: (query) => dataProvider.getNews(query),
318
+ },
319
+ }));
320
+
321
+ const plugins = getRendererBuiltinPlugins();
322
+ for (const plugin of plugins) {
323
+ measurePerf("startup.services.register-plugin", () => {
324
+ void pluginRegistry.register(plugin);
325
+ }, { pluginId: plugin.id });
326
+ }
327
+ measurePerf("startup.services.news-start", () => {
328
+ newsService.start();
329
+ });
330
+ servicesLog.info("create desktop web services complete", { pluginCount: plugins.length });
331
+
332
+ return {
333
+ persistence: persistence as never,
334
+ tickerRepository: tickerRepository as never,
335
+ providerRouter: dataProvider as never,
336
+ dataProvider,
337
+ marketData,
338
+ pluginRegistry,
339
+ newsService,
340
+ destroy() {
341
+ setSharedMarketDataCoordinator(null);
342
+ setSharedNewsService(null);
343
+ newsService.stop();
344
+ pluginRegistry.destroy();
345
+ persistence.close();
346
+ },
347
+ };
348
+ }
@@ -0,0 +1,12 @@
1
+ import type { DesktopApplicationMenuBridge } from "../../../types/desktop-menu";
2
+ import { onApplicationMenuSelect } from "./backend-rpc";
3
+
4
+ export function createApplicationMenuBridge(): DesktopApplicationMenuBridge {
5
+ return {
6
+ subscribe(listener) {
7
+ return onApplicationMenuSelect((message) => {
8
+ listener(message.command);
9
+ });
10
+ },
11
+ };
12
+ }