gloomberb 0.5.0 → 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 -13
  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 +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  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 +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  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 +186 -6
  440. package/src/utils/api-client.ts +433 -33
  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,173 @@
1
+ /// <reference lib="dom" />
2
+ import { Electroview } from "electrobun/view";
3
+ import { measurePerfAsync } from "../../../utils/perf-marks";
4
+ import {
5
+ type ApplicationMenuSelectMessage,
6
+ type CapabilityEventMessage,
7
+ type ContextMenuSelectMessage,
8
+ type DesktopDockPreviewMessage,
9
+ type DesktopStateMessage,
10
+ type ElectrobunBackendInit,
11
+ type ElectrobunDesktopRpcSchema,
12
+ type UpdateProgressMessage,
13
+ } from "../shared/protocol";
14
+ import { decodeRpcValue, encodeRpcValue } from "./rpc-codec";
15
+
16
+ type ContextMenuSelectListener = (message: ContextMenuSelectMessage) => void;
17
+ type ApplicationMenuSelectListener = (message: ApplicationMenuSelectMessage) => void;
18
+ type DesktopStateListener = (message: DesktopStateMessage) => void;
19
+ type DesktopDockPreviewListener = (message: DesktopDockPreviewMessage) => void;
20
+ type UpdateProgressListener = (message: UpdateProgressMessage) => void;
21
+ type CapabilityEventListener = (message: CapabilityEventMessage) => void;
22
+
23
+ let initSnapshot: ElectrobunBackendInit | null = null;
24
+ const contextMenuSelectListeners = new Map<string, Set<ContextMenuSelectListener>>();
25
+ const applicationMenuSelectListeners = new Set<ApplicationMenuSelectListener>();
26
+ const desktopStateListeners = new Set<DesktopStateListener>();
27
+ const desktopDockPreviewListeners = new Set<DesktopDockPreviewListener>();
28
+ const updateProgressListeners = new Set<UpdateProgressListener>();
29
+ const capabilityEventListeners = new Map<string, Set<CapabilityEventListener>>();
30
+
31
+ function dispatch<T>(
32
+ listeners: Map<string, Set<(value: T) => void>>,
33
+ key: string,
34
+ value: T,
35
+ ): void {
36
+ for (const listener of listeners.get(key) ?? []) {
37
+ listener(value);
38
+ }
39
+ }
40
+
41
+ function subscribe<T>(
42
+ listeners: Map<string, Set<(value: T) => void>>,
43
+ key: string,
44
+ listener: (value: T) => void,
45
+ ): () => void {
46
+ if (!listeners.has(key)) {
47
+ listeners.set(key, new Set());
48
+ }
49
+ listeners.get(key)!.add(listener);
50
+ return () => {
51
+ const bucket = listeners.get(key);
52
+ if (!bucket) return;
53
+ bucket.delete(listener);
54
+ if (bucket.size === 0) {
55
+ listeners.delete(key);
56
+ }
57
+ };
58
+ }
59
+
60
+ const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
61
+ maxRequestTime: 120_000,
62
+ handlers: {
63
+ requests: {},
64
+ messages: {
65
+ "context-menu.select": (message) => {
66
+ dispatch(contextMenuSelectListeners, message.requestId, message);
67
+ },
68
+ "application-menu.select": (message) => {
69
+ const decoded = { command: decodeRpcValue<ApplicationMenuSelectMessage["command"]>(message.command) };
70
+ for (const listener of applicationMenuSelectListeners) {
71
+ listener(decoded);
72
+ }
73
+ },
74
+ "desktop.state": (message) => {
75
+ for (const listener of desktopStateListeners) {
76
+ listener({ snapshot: decodeRpcValue(message.snapshot) });
77
+ }
78
+ },
79
+ "desktop.dockPreview": (message) => {
80
+ for (const listener of desktopDockPreviewListeners) {
81
+ listener({ preview: decodeRpcValue(message.preview) });
82
+ }
83
+ },
84
+ "update.progress": (message) => {
85
+ const decoded = { progress: decodeRpcValue<UpdateProgressMessage["progress"]>(message.progress) };
86
+ for (const listener of updateProgressListeners) {
87
+ listener(decoded);
88
+ }
89
+ },
90
+ "capability.event": (message) => {
91
+ dispatch(capabilityEventListeners, message.subscriptionId, {
92
+ subscriptionId: message.subscriptionId,
93
+ event: decodeRpcValue(message.event),
94
+ });
95
+ },
96
+ },
97
+ },
98
+ });
99
+
100
+ const electroview = new Electroview({ rpc });
101
+
102
+ async function waitForBridgeReady(): Promise<void> {
103
+ const start = Date.now();
104
+ while (Date.now() - start < 5_000) {
105
+ if (electroview.bunSocket?.readyState === WebSocket.OPEN) {
106
+ return;
107
+ }
108
+ await new Promise((resolve) => setTimeout(resolve, 25));
109
+ }
110
+ throw new Error("Electrobun RPC socket did not open in time.");
111
+ }
112
+
113
+ export async function backendRequest<T = unknown>(method: string, payload: unknown = null): Promise<T> {
114
+ const result = await measurePerfAsync("electrobun.rpc.request", async () => {
115
+ await waitForBridgeReady();
116
+ return rpc.request["backend.request"]({
117
+ method,
118
+ payload: encodeRpcValue(payload),
119
+ });
120
+ }, { method });
121
+ return decodeRpcValue<T>(result);
122
+ }
123
+
124
+ export async function initElectrobunBackend(payload?: { kind?: "main" | "detached"; paneId?: string }): Promise<ElectrobunBackendInit> {
125
+ initSnapshot = await backendRequest<ElectrobunBackendInit>("init", payload ?? null);
126
+ return initSnapshot;
127
+ }
128
+
129
+ export function getElectrobunBackendInitSnapshot(): ElectrobunBackendInit | null {
130
+ return initSnapshot;
131
+ }
132
+
133
+ export function onCapabilityEvent(
134
+ subscriptionId: string,
135
+ listener: (message: CapabilityEventMessage) => void,
136
+ ): () => void {
137
+ return subscribe(capabilityEventListeners, subscriptionId, listener);
138
+ }
139
+
140
+ export function onContextMenuSelect(
141
+ requestId: string,
142
+ listener: (message: ContextMenuSelectMessage) => void,
143
+ ): () => void {
144
+ return subscribe(contextMenuSelectListeners, requestId, listener);
145
+ }
146
+
147
+ export function onApplicationMenuSelect(listener: ApplicationMenuSelectListener): () => void {
148
+ applicationMenuSelectListeners.add(listener);
149
+ return () => {
150
+ applicationMenuSelectListeners.delete(listener);
151
+ };
152
+ }
153
+
154
+ export function onDesktopState(listener: DesktopStateListener): () => void {
155
+ desktopStateListeners.add(listener);
156
+ return () => {
157
+ desktopStateListeners.delete(listener);
158
+ };
159
+ }
160
+
161
+ export function onDesktopDockPreview(listener: DesktopDockPreviewListener): () => void {
162
+ desktopDockPreviewListeners.add(listener);
163
+ return () => {
164
+ desktopDockPreviewListeners.delete(listener);
165
+ };
166
+ }
167
+
168
+ export function onUpdateProgress(listener: UpdateProgressListener): () => void {
169
+ updateProgressListeners.add(listener);
170
+ return () => {
171
+ updateProgressListeners.delete(listener);
172
+ };
173
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ setBrokerRemoteClient,
3
+ type BrokerRemoteClient,
4
+ } from "../../../brokers/remote-broker-adapter";
5
+ import {
6
+ BROKER_CAPABILITY_ID,
7
+ type BrokerQuoteEvent,
8
+ type BrokerRemoteEvent,
9
+ type BrokerStatusEvent,
10
+ } from "../../../capabilities";
11
+ import type { BrokerConnectionStatus } from "../../../types/broker";
12
+ import { backendRequest, onCapabilityEvent } from "./backend-rpc";
13
+
14
+ const statuses = new Map<string, BrokerConnectionStatus>();
15
+ const statusSubscriptions = new Map<string, StatusSubscription>();
16
+ const STATUS_SUBSCRIPTION_TEARDOWN_DELAY_MS = 250;
17
+ let nextSubscriptionId = 1;
18
+
19
+ type StatusSubscription = {
20
+ subscriptionId: string;
21
+ listeners: Set<() => void>;
22
+ disposeEvents: () => void;
23
+ teardownTimer: ReturnType<typeof setTimeout> | null;
24
+ };
25
+
26
+ function isBrokerStatusEvent(event: BrokerRemoteEvent): event is BrokerStatusEvent {
27
+ return event.kind === "status";
28
+ }
29
+
30
+ function isBrokerQuoteEvent(event: BrokerRemoteEvent): event is BrokerQuoteEvent {
31
+ return event.kind === "quote";
32
+ }
33
+
34
+ function invokeBrokerCapability<T>(operationId: string, payload: unknown): Promise<T> {
35
+ return backendRequest<T>("capability.invoke", {
36
+ capabilityId: BROKER_CAPABILITY_ID,
37
+ operationId,
38
+ payload,
39
+ });
40
+ }
41
+
42
+ function subscribeBrokerCapability(
43
+ subscriptionId: string,
44
+ operationId: string,
45
+ payload: unknown,
46
+ ): Promise<void> {
47
+ return backendRequest("capability.subscribe", {
48
+ subscriptionId,
49
+ capabilityId: BROKER_CAPABILITY_ID,
50
+ operationId,
51
+ payload,
52
+ });
53
+ }
54
+
55
+ function disposeStatusSubscription(instanceId: string, entry: StatusSubscription): void {
56
+ if (statusSubscriptions.get(instanceId) !== entry) return;
57
+ entry.disposeEvents();
58
+ if (entry.teardownTimer) clearTimeout(entry.teardownTimer);
59
+ statusSubscriptions.delete(instanceId);
60
+ void backendRequest("capability.unsubscribe", { subscriptionId: entry.subscriptionId }).catch(() => {});
61
+ }
62
+
63
+ function getStatusSubscription(instanceId: string): StatusSubscription {
64
+ const current = statusSubscriptions.get(instanceId);
65
+ if (current) return current;
66
+
67
+ const subscriptionId = `broker-status:${instanceId}:${nextSubscriptionId++}`;
68
+ const entry: StatusSubscription = {
69
+ subscriptionId,
70
+ listeners: new Set(),
71
+ disposeEvents: () => {},
72
+ teardownTimer: null,
73
+ };
74
+ entry.disposeEvents = onCapabilityEvent(subscriptionId, (message) => {
75
+ const event = message.event as BrokerRemoteEvent;
76
+ if (!isBrokerStatusEvent(event)) return;
77
+ statuses.set(event.instanceId, event.status);
78
+ for (const listener of entry.listeners) {
79
+ listener();
80
+ }
81
+ });
82
+ statusSubscriptions.set(instanceId, entry);
83
+
84
+ void subscribeBrokerCapability(subscriptionId, "status", { instanceId }).catch((error) => {
85
+ disposeStatusSubscription(instanceId, entry);
86
+ console.error("Failed to subscribe to broker status", error);
87
+ });
88
+
89
+ return entry;
90
+ }
91
+
92
+ const client: BrokerRemoteClient = {
93
+ invoke(operationInstanceId, operation, args = []) {
94
+ return invokeBrokerCapability("invoke", {
95
+ instanceId: operationInstanceId,
96
+ operation,
97
+ args,
98
+ });
99
+ },
100
+
101
+ getStatus(instanceId) {
102
+ return statuses.get(instanceId) ?? null;
103
+ },
104
+
105
+ subscribeStatus(instanceId, listener) {
106
+ const entry = getStatusSubscription(instanceId);
107
+ if (entry.teardownTimer) {
108
+ clearTimeout(entry.teardownTimer);
109
+ entry.teardownTimer = null;
110
+ }
111
+ entry.listeners.add(listener);
112
+ return () => {
113
+ entry.listeners.delete(listener);
114
+ if (entry.listeners.size > 0 || entry.teardownTimer) return;
115
+ entry.teardownTimer = setTimeout(() => {
116
+ entry.teardownTimer = null;
117
+ if (entry.listeners.size === 0) {
118
+ disposeStatusSubscription(instanceId, entry);
119
+ }
120
+ }, STATUS_SUBSCRIPTION_TEARDOWN_DELAY_MS);
121
+ };
122
+ },
123
+
124
+ subscribeQuotes(instanceId, targets, onQuote) {
125
+ const subscriptionId = `broker-quotes:${instanceId}:${nextSubscriptionId++}`;
126
+ const disposeEvents = onCapabilityEvent(subscriptionId, (message) => {
127
+ const event = message.event as BrokerRemoteEvent;
128
+ if (!isBrokerQuoteEvent(event)) return;
129
+ onQuote(event.target, event.quote);
130
+ });
131
+ void subscribeBrokerCapability(subscriptionId, "quotes", { instanceId, targets }).catch((error) => {
132
+ disposeEvents();
133
+ console.error("Failed to subscribe to broker quotes", error);
134
+ });
135
+ return () => {
136
+ disposeEvents();
137
+ void backendRequest("capability.unsubscribe", { subscriptionId }).catch(() => {});
138
+ };
139
+ },
140
+
141
+ async removeInstance(instanceId) {
142
+ await invokeBrokerCapability("removeInstance", { instanceId });
143
+ statuses.delete(instanceId);
144
+ },
145
+
146
+ async destroyAll() {
147
+ await invokeBrokerCapability("destroyAll", {});
148
+ statuses.clear();
149
+ },
150
+ };
151
+
152
+ export function installElectrobunBrokerRemoteClient(): void {
153
+ setBrokerRemoteClient(client);
154
+ }
@@ -0,0 +1,35 @@
1
+ import { setConfigStoreHost, type ConfigStoreHost } from "../../../data/config-store";
2
+ import type { AppConfig } from "../../../types/config";
3
+ import { backendRequest, getElectrobunBackendInitSnapshot } from "./backend-rpc";
4
+
5
+ const electrobunConfigStoreHost: ConfigStoreHost = {
6
+ async getDataDir() {
7
+ return getElectrobunBackendInitSnapshot()?.config.dataDir ?? null;
8
+ },
9
+ async loadConfig() {
10
+ const config = getElectrobunBackendInitSnapshot()?.config;
11
+ if (!config) throw new Error("Electrobun backend has not initialized config.");
12
+ return config;
13
+ },
14
+ async saveConfig(config: AppConfig) {
15
+ await backendRequest("config.save", { config });
16
+ },
17
+ async initDataDir() {
18
+ const config = getElectrobunBackendInitSnapshot()?.config;
19
+ if (!config) throw new Error("Electrobun backend has not initialized config.");
20
+ return config;
21
+ },
22
+ async resetAllData(dataDir: string) {
23
+ await backendRequest("config.resetAllData", { dataDir });
24
+ },
25
+ async exportConfig(config: AppConfig, destPath: string) {
26
+ await backendRequest("config.export", { config, destPath });
27
+ },
28
+ async importConfig(dataDir: string, srcPath: string) {
29
+ return backendRequest<AppConfig>("config.import", { dataDir, srcPath });
30
+ },
31
+ };
32
+
33
+ export function installElectrobunConfigStoreHost(): void {
34
+ setConfigStoreHost(electrobunConfigStoreHost);
35
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { contextMenuDivider, type ContextMenuItem } from "../../../types/context-menu";
3
+ import { ELECTROBUN_CONTEXT_MENU_ACTION } from "../shared/protocol";
4
+ import {
5
+ DesktopContextMenuActionScope,
6
+ prepareDesktopContextMenu,
7
+ type DesktopContextMenuSelectMessage,
8
+ } from "./context-menu";
9
+
10
+ describe("Electrobun desktop context menu serialization", () => {
11
+ test("preserves roles, dividers, submenu state, accelerators, and checked state", () => {
12
+ const items: ContextMenuItem[] = [
13
+ { type: "role", role: "copy", label: "Copy", accelerator: "CmdOrCtrl+C" },
14
+ contextMenuDivider(),
15
+ {
16
+ id: "tools",
17
+ label: "Tools",
18
+ enabled: true,
19
+ checked: true,
20
+ accelerator: "CmdOrCtrl+T",
21
+ tooltip: "Open tools",
22
+ submenu: [
23
+ { id: "nested", label: "Nested", enabled: false },
24
+ ],
25
+ },
26
+ ];
27
+
28
+ const prepared = prepareDesktopContextMenu(items, "request-1");
29
+
30
+ expect(prepared.menu).toEqual([
31
+ {
32
+ type: "normal",
33
+ label: "Copy",
34
+ role: "copy",
35
+ enabled: undefined,
36
+ checked: undefined,
37
+ hidden: undefined,
38
+ tooltip: undefined,
39
+ accelerator: "CmdOrCtrl+C",
40
+ },
41
+ { type: "divider" },
42
+ {
43
+ type: "normal",
44
+ label: "Tools",
45
+ tooltip: "Open tools",
46
+ enabled: true,
47
+ checked: true,
48
+ hidden: undefined,
49
+ accelerator: "CmdOrCtrl+T",
50
+ action: undefined,
51
+ data: undefined,
52
+ submenu: [
53
+ {
54
+ type: "normal",
55
+ label: "Nested",
56
+ tooltip: undefined,
57
+ enabled: false,
58
+ checked: undefined,
59
+ hidden: undefined,
60
+ accelerator: undefined,
61
+ action: undefined,
62
+ data: undefined,
63
+ submenu: undefined,
64
+ },
65
+ ],
66
+ },
67
+ ]);
68
+ expect(prepared.actions.size).toBe(0);
69
+ });
70
+
71
+ test("custom action items get request-scoped action data and roles do not register callbacks", () => {
72
+ const action = () => {};
73
+ const prepared = prepareDesktopContextMenu([
74
+ { type: "role", role: "paste" },
75
+ { id: "open", label: "Open", onSelect: action },
76
+ ], "request-2");
77
+
78
+ expect(prepared.menu[0]).toMatchObject({ role: "paste" });
79
+ expect(prepared.menu[1]).toMatchObject({
80
+ action: `${ELECTROBUN_CONTEXT_MENU_ACTION}:request-2:open`,
81
+ data: { requestId: "request-2", itemId: "open" },
82
+ });
83
+ expect(prepared.actions.get("open")).toBe(action);
84
+ expect(prepared.actions.size).toBe(1);
85
+ });
86
+ });
87
+
88
+ describe("DesktopContextMenuActionScope", () => {
89
+ test("selection messages call the matching callback once", () => {
90
+ let listener: ((message: DesktopContextMenuSelectMessage) => void) | null = null;
91
+ let disposeCount = 0;
92
+ let runCount = 0;
93
+ const scope = new DesktopContextMenuActionScope(
94
+ (_requestId, nextListener) => {
95
+ listener = nextListener;
96
+ return () => {
97
+ disposeCount += 1;
98
+ };
99
+ },
100
+ 1000,
101
+ (() => 1) as never,
102
+ (() => {}) as never,
103
+ );
104
+
105
+ scope.bind("request-3", new Map([["item", () => { runCount += 1; }]]));
106
+ listener?.({ requestId: "request-3", itemId: "item" });
107
+ listener?.({ requestId: "request-3", itemId: "item" });
108
+
109
+ expect(runCount).toBe(1);
110
+ expect(disposeCount).toBe(1);
111
+ });
112
+
113
+ test("stale callbacks are cleaned up after the timeout", () => {
114
+ let listener: ((message: DesktopContextMenuSelectMessage) => void) | null = null;
115
+ let timeoutCallback: (() => void) | null = null;
116
+ let disposeCount = 0;
117
+ let runCount = 0;
118
+ const scope = new DesktopContextMenuActionScope(
119
+ (_requestId, nextListener) => {
120
+ listener = nextListener;
121
+ return () => {
122
+ disposeCount += 1;
123
+ };
124
+ },
125
+ 1000,
126
+ ((callback: () => void) => {
127
+ timeoutCallback = callback;
128
+ return 1;
129
+ }) as never,
130
+ (() => {}) as never,
131
+ );
132
+
133
+ scope.bind("request-4", new Map([["item", () => { runCount += 1; }]]));
134
+ timeoutCallback?.();
135
+ listener?.({ requestId: "request-4", itemId: "item" });
136
+
137
+ expect(disposeCount).toBe(1);
138
+ expect(runCount).toBe(0);
139
+ });
140
+ });
@@ -0,0 +1,164 @@
1
+ import type { ContextMenuActionItem, ContextMenuItem, ContextMenuRoleItem } from "../../../types/context-menu";
2
+ import { ELECTROBUN_CONTEXT_MENU_ACTION } from "../shared/protocol";
3
+
4
+ export type DesktopContextMenuItem =
5
+ | { type: "divider" }
6
+ | {
7
+ type?: "normal";
8
+ label?: string;
9
+ tooltip?: string;
10
+ action?: string;
11
+ role?: string;
12
+ data?: unknown;
13
+ submenu?: DesktopContextMenuItem[];
14
+ enabled?: boolean;
15
+ checked?: boolean;
16
+ hidden?: boolean;
17
+ accelerator?: string;
18
+ };
19
+
20
+ export interface PreparedDesktopContextMenu {
21
+ menu: DesktopContextMenuItem[];
22
+ actions: Map<string, () => void | Promise<void>>;
23
+ }
24
+
25
+ export interface DesktopContextMenuSelectMessage {
26
+ requestId: string;
27
+ itemId: string;
28
+ }
29
+
30
+ type ContextMenuSelectSubscribe = (
31
+ requestId: string,
32
+ listener: (message: DesktopContextMenuSelectMessage) => void,
33
+ ) => () => void;
34
+
35
+ type ContextMenuTimeout = ReturnType<typeof globalThis.setTimeout>;
36
+ type ScheduleTimeout = (callback: () => void, delayMs: number) => ContextMenuTimeout;
37
+ type ClearScheduledTimeout = (timeout: ContextMenuTimeout) => void;
38
+
39
+ function scheduleContextMenuTimeout(callback: () => void, delayMs: number): ContextMenuTimeout {
40
+ return globalThis.setTimeout(callback, delayMs);
41
+ }
42
+
43
+ function clearContextMenuTimeout(timeout: ContextMenuTimeout): void {
44
+ globalThis.clearTimeout(timeout);
45
+ }
46
+
47
+ function actionIdFor(path: readonly number[], item: ContextMenuActionItem): string {
48
+ return item.id || path.join(".");
49
+ }
50
+
51
+ function nativeActionFor(requestId: string, itemId: string): string {
52
+ return `${ELECTROBUN_CONTEXT_MENU_ACTION}:${encodeURIComponent(requestId)}:${encodeURIComponent(itemId)}`;
53
+ }
54
+
55
+ function roleItemToDesktop(item: ContextMenuRoleItem): DesktopContextMenuItem {
56
+ return {
57
+ type: "normal",
58
+ label: item.label,
59
+ role: item.role,
60
+ enabled: item.enabled,
61
+ checked: item.checked,
62
+ hidden: item.hidden,
63
+ tooltip: item.tooltip,
64
+ accelerator: item.accelerator,
65
+ };
66
+ }
67
+
68
+ function prepareItems(
69
+ items: readonly ContextMenuItem[],
70
+ requestId: string,
71
+ path: number[],
72
+ actions: Map<string, () => void | Promise<void>>,
73
+ ): DesktopContextMenuItem[] {
74
+ return items.map((item, index): DesktopContextMenuItem => {
75
+ if (item.type === "divider") return { type: "divider" };
76
+ if (item.type === "role") return roleItemToDesktop(item);
77
+
78
+ const itemPath = [...path, index];
79
+ const itemId = actionIdFor(itemPath, item);
80
+ if (item.onSelect) {
81
+ actions.set(itemId, item.onSelect);
82
+ }
83
+
84
+ return {
85
+ type: "normal",
86
+ label: item.label,
87
+ tooltip: item.tooltip,
88
+ enabled: item.enabled,
89
+ checked: item.checked,
90
+ hidden: item.hidden,
91
+ accelerator: item.accelerator,
92
+ action: item.onSelect ? nativeActionFor(requestId, itemId) : undefined,
93
+ data: item.onSelect ? { requestId, itemId } : undefined,
94
+ submenu: item.submenu ? prepareItems(item.submenu, requestId, itemPath, actions) : undefined,
95
+ };
96
+ });
97
+ }
98
+
99
+ export function prepareDesktopContextMenu(
100
+ items: readonly ContextMenuItem[],
101
+ requestId: string,
102
+ ): PreparedDesktopContextMenu {
103
+ const actions = new Map<string, () => void | Promise<void>>();
104
+ return {
105
+ menu: prepareItems(items, requestId, [], actions),
106
+ actions,
107
+ };
108
+ }
109
+
110
+ export function createContextMenuRequestId(): string {
111
+ return `ctx:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
112
+ }
113
+
114
+ export class DesktopContextMenuActionScope {
115
+ private active:
116
+ | {
117
+ requestId: string;
118
+ actions: Map<string, () => void | Promise<void>>;
119
+ dispose: () => void;
120
+ timeout: ContextMenuTimeout;
121
+ }
122
+ | null = null;
123
+
124
+ constructor(
125
+ private readonly subscribe: ContextMenuSelectSubscribe,
126
+ private readonly ttlMs: number,
127
+ private readonly scheduleTimeout: ScheduleTimeout = scheduleContextMenuTimeout,
128
+ private readonly clearScheduledTimeout: ClearScheduledTimeout = clearContextMenuTimeout,
129
+ ) {}
130
+
131
+ clear(): void {
132
+ if (!this.active) return;
133
+ this.active.dispose();
134
+ this.clearScheduledTimeout(this.active.timeout);
135
+ this.active = null;
136
+ }
137
+
138
+ clearRequest(requestId: string): void {
139
+ if (this.active?.requestId === requestId) {
140
+ this.clear();
141
+ }
142
+ }
143
+
144
+ bind(requestId: string, actions: Map<string, () => void | Promise<void>>): void {
145
+ this.clear();
146
+ if (actions.size === 0) return;
147
+
148
+ const dispose = this.subscribe(requestId, (message) => {
149
+ const action = this.active?.actions.get(message.itemId);
150
+ this.clear();
151
+ if (!action) return;
152
+ void action();
153
+ });
154
+ const timeout = this.scheduleTimeout(() => {
155
+ this.clearRequest(requestId);
156
+ }, this.ttlMs);
157
+ this.active = {
158
+ requestId,
159
+ actions,
160
+ dispose,
161
+ timeout,
162
+ };
163
+ }
164
+ }