gloomberb 0.10.1 → 0.10.2

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 (351) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,7 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { shouldDeliverShortcut, type KeyEventLike } from "./input";
2
+ import {
3
+ createShortcutRegistry,
4
+ shouldDeliverShortcut,
5
+ type KeyEventLike,
6
+ type ShortcutOptions,
7
+ type ShortcutRegistry,
8
+ } from "./input";
3
9
 
4
10
  function keyEvent(overrides: Partial<KeyEventLike> = {}): KeyEventLike {
11
+ let defaultPrevented = false;
12
+ let propagationStopped = false;
5
13
  return {
6
14
  key: "k",
7
15
  name: "k",
@@ -9,12 +17,36 @@ function keyEvent(overrides: Partial<KeyEventLike> = {}): KeyEventLike {
9
17
  shift: false,
10
18
  alt: false,
11
19
  meta: false,
12
- preventDefault: () => {},
13
- stopPropagation: () => {},
20
+ get defaultPrevented() {
21
+ return defaultPrevented;
22
+ },
23
+ get propagationStopped() {
24
+ return propagationStopped;
25
+ },
26
+ preventDefault: () => {
27
+ defaultPrevented = true;
28
+ },
29
+ stopPropagation: () => {
30
+ propagationStopped = true;
31
+ },
14
32
  ...overrides,
15
33
  };
16
34
  }
17
35
 
36
+ function registerShortcut(
37
+ registry: ShortcutRegistry,
38
+ handler: (event: KeyEventLike) => void,
39
+ options: ShortcutOptions & { isEnabled?: () => boolean } = {},
40
+ ): () => void {
41
+ return registry.register({
42
+ handler,
43
+ isEnabled: options.isEnabled ?? (() => options.enabled !== false),
44
+ allowsEditable: () => options.allowEditable === true,
45
+ phase: options.phase ?? "normal",
46
+ scope: options.scope,
47
+ });
48
+ }
49
+
18
50
  describe("shouldDeliverShortcut", () => {
19
51
  test("keeps plain app shortcuts out of editable targets by default", () => {
20
52
  expect(shouldDeliverShortcut(keyEvent({ targetEditable: true }), false)).toBe(false);
@@ -27,3 +59,94 @@ describe("shouldDeliverShortcut", () => {
27
59
  expect(shouldDeliverShortcut(keyEvent({ targetEditable: true, meta: true }), false)).toBe(true);
28
60
  });
29
61
  });
62
+
63
+ describe("shortcut registry", () => {
64
+ test("dispatches capture, normal, and bubble phases in order", () => {
65
+ const calls: string[] = [];
66
+ const registry = createShortcutRegistry();
67
+ registerShortcut(registry, () => calls.push("normal-1"));
68
+ registerShortcut(registry, () => calls.push("after"), { phase: "after" });
69
+ registerShortcut(registry, () => calls.push("before"), { phase: "before" });
70
+ registerShortcut(registry, () => calls.push("normal-2"));
71
+
72
+ registry.dispatch(keyEvent());
73
+
74
+ expect(calls).toEqual(["before", "normal-1", "normal-2", "after"]);
75
+ });
76
+
77
+ test("skips the bubble phase after an earlier handler prevents the default action", () => {
78
+ const calls: string[] = [];
79
+ const registry = createShortcutRegistry();
80
+ registerShortcut(registry, (event) => {
81
+ calls.push("normal");
82
+ event.preventDefault();
83
+ });
84
+ registerShortcut(registry, () => calls.push("after"), { phase: "after" });
85
+
86
+ registry.dispatch(keyEvent());
87
+
88
+ expect(calls).toEqual(["normal"]);
89
+ });
90
+
91
+ test("gives the newest scope first refusal and keeps a consumed event in that scope", () => {
92
+ const calls: string[] = [];
93
+ const registry = createShortcutRegistry();
94
+ registerShortcut(registry, () => calls.push("older"), { scope: "older-dialog" });
95
+ registerShortcut(registry, () => calls.push("global"));
96
+ registerShortcut(registry, (event) => {
97
+ calls.push("newer-1");
98
+ event.preventDefault();
99
+ }, { scope: "newer-dialog" });
100
+ registerShortcut(registry, () => calls.push("newer-2"), { scope: "newer-dialog" });
101
+
102
+ registry.dispatch(keyEvent());
103
+
104
+ expect(calls).toEqual(["newer-1", "newer-2"]);
105
+ });
106
+
107
+ test("falls through scopes until a handler consumes the event", () => {
108
+ const calls: string[] = [];
109
+ const registry = createShortcutRegistry();
110
+ registerShortcut(registry, () => calls.push("older"), { scope: "older" });
111
+ registerShortcut(registry, () => calls.push("global"));
112
+ registerShortcut(registry, () => calls.push("newer"), { scope: "newer" });
113
+
114
+ registry.dispatch(keyEvent());
115
+
116
+ expect(calls).toEqual(["newer", "older", "global"]);
117
+ });
118
+
119
+ test("applies editable and enabled gates before scope ordering", () => {
120
+ const calls: string[] = [];
121
+ const registry = createShortcutRegistry();
122
+ registerShortcut(registry, () => calls.push("editable-blocked"), { scope: "newest" });
123
+ registerShortcut(registry, () => calls.push("disabled"), {
124
+ scope: "newest",
125
+ enabled: false,
126
+ });
127
+ registerShortcut(registry, () => calls.push("allowed"), {
128
+ scope: "older",
129
+ allowEditable: true,
130
+ });
131
+
132
+ registry.dispatch(keyEvent({ targetEditable: true }));
133
+
134
+ expect(calls).toEqual(["allowed"]);
135
+ });
136
+
137
+ test("stops propagation immediately and removes cleaned-up handlers", () => {
138
+ const calls: string[] = [];
139
+ const registry = createShortcutRegistry();
140
+ const unregister = registerShortcut(registry, () => calls.push("removed"));
141
+ unregister();
142
+ registerShortcut(registry, (event) => {
143
+ calls.push("stop");
144
+ event.stopPropagation();
145
+ });
146
+ registerShortcut(registry, () => calls.push("skipped"));
147
+
148
+ registry.dispatch(keyEvent());
149
+
150
+ expect(calls).toEqual(["stop"]);
151
+ });
152
+ });
@@ -1,4 +1,11 @@
1
- import { createContext, createElement, useContext, type ReactNode } from "react";
1
+ import {
2
+ createContext,
3
+ createElement,
4
+ useContext,
5
+ useLayoutEffect,
6
+ useRef,
7
+ type ReactNode,
8
+ } from "react";
2
9
 
3
10
  export interface KeyEventLike {
4
11
  key: string;
@@ -18,11 +25,123 @@ export interface KeyEventLike {
18
25
 
19
26
  export interface ShortcutOptions {
20
27
  enabled?: boolean;
28
+ /**
29
+ * Groups related handlers into one interaction surface. Within each phase,
30
+ * newer enabled scopes receive events before older scopes and unscoped
31
+ * handlers. Once a scoped handler prevents the default action, only that
32
+ * scope receives the rest of the dispatch.
33
+ */
21
34
  scope?: string;
22
35
  phase?: "before" | "normal" | "after";
23
36
  allowEditable?: boolean;
24
37
  }
25
38
 
39
+ interface ShortcutRegistration {
40
+ handler(event: KeyEventLike): void;
41
+ isEnabled(): boolean;
42
+ allowsEditable(): boolean;
43
+ scope?: string;
44
+ phase: NonNullable<ShortcutOptions["phase"]>;
45
+ }
46
+
47
+ interface ShortcutEntry extends ShortcutRegistration {
48
+ order: number;
49
+ }
50
+
51
+ export interface ShortcutRegistry {
52
+ register(registration: ShortcutRegistration): () => void;
53
+ dispatch(event: KeyEventLike): void;
54
+ }
55
+
56
+ const SHORTCUT_PHASES: ReadonlyArray<ShortcutEntry["phase"]> = [
57
+ "before",
58
+ "normal",
59
+ "after",
60
+ ];
61
+
62
+ function normalizeShortcutScope(scope: string | undefined): string | undefined {
63
+ const normalized = scope?.trim();
64
+ return normalized || undefined;
65
+ }
66
+
67
+ function orderShortcutEntries(entries: ShortcutEntry[]): ShortcutEntry[] {
68
+ const scopePriority = new Map<string, number>();
69
+ for (const entry of entries) {
70
+ if (!entry.scope) continue;
71
+ scopePriority.set(entry.scope, Math.max(scopePriority.get(entry.scope) ?? 0, entry.order));
72
+ }
73
+
74
+ return entries.sort((left, right) => {
75
+ if (left.scope === right.scope) return left.order - right.order;
76
+ if (!left.scope) return 1;
77
+ if (!right.scope) return -1;
78
+ return (scopePriority.get(right.scope) ?? 0) - (scopePriority.get(left.scope) ?? 0);
79
+ });
80
+ }
81
+
82
+ export function createShortcutRegistry(): ShortcutRegistry {
83
+ let nextOrder = 1;
84
+ const entries = new Map<number, ShortcutEntry>();
85
+
86
+ return {
87
+ register(registration) {
88
+ const order = nextOrder++;
89
+ entries.set(order, {
90
+ ...registration,
91
+ scope: normalizeShortcutScope(registration.scope),
92
+ order,
93
+ });
94
+ return () => {
95
+ entries.delete(order);
96
+ };
97
+ },
98
+ dispatch(event) {
99
+ const eligible = orderShortcutEntries(
100
+ [...entries.values()].filter((entry) => (
101
+ entry.isEnabled() && shouldDeliverShortcut(event, entry.allowsEditable())
102
+ )),
103
+ );
104
+ let claimedScope: string | undefined;
105
+
106
+ for (const phase of SHORTCUT_PHASES) {
107
+ if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
108
+ for (const entry of eligible) {
109
+ if (entry.phase !== phase) continue;
110
+ if (claimedScope && entry.scope !== claimedScope) continue;
111
+
112
+ const wasDefaultPrevented = event.defaultPrevented === true;
113
+ entry.handler(event);
114
+ if (!wasDefaultPrevented && event.defaultPrevented && entry.scope) {
115
+ claimedScope = entry.scope;
116
+ }
117
+ if (event.propagationStopped) return;
118
+ }
119
+ }
120
+ },
121
+ };
122
+ }
123
+
124
+ export function useRegisteredShortcut(
125
+ registry: ShortcutRegistry,
126
+ handler: (event: KeyEventLike) => void,
127
+ options?: ShortcutOptions,
128
+ ): void {
129
+ const handlerRef = useRef(handler);
130
+ const enabledRef = useRef(options?.enabled !== false);
131
+ const allowEditableRef = useRef(options?.allowEditable === true);
132
+ handlerRef.current = handler;
133
+ enabledRef.current = options?.enabled !== false;
134
+ allowEditableRef.current = options?.allowEditable === true;
135
+
136
+ useLayoutEffect(() => registry.register({
137
+ handler: (event) => handlerRef.current(event),
138
+ isEnabled: () => enabledRef.current,
139
+ allowsEditable: () => allowEditableRef.current,
140
+ phase: options?.phase ?? "normal",
141
+ scope: options?.scope,
142
+ }), [options?.phase, options?.scope, registry]);
143
+ }
144
+
26
145
  export interface InputHost {
27
146
  useShortcut(
28
147
  handler: (event: KeyEventLike) => void,
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useEffect, useMemo } from "react";
1
+ import { useEffect, useMemo } from "react";
2
2
  import type { Dispatch, ReactNode } from "react";
3
3
  import type { PluginRegistry } from "../plugins/registry";
4
4
  import type { AppAction, AppState } from "../state/app/context";
@@ -9,14 +9,7 @@ import type { RemoteControlRequest, RemoteControlResponse } from "./types";
9
9
 
10
10
  export type RemoteControlHandler = (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
11
11
 
12
- const RemoteControlHandlerContext = createContext<RemoteControlHandler | null>(null);
13
-
14
- export function useRemoteControlHandler(): RemoteControlHandler | null {
15
- return useContext(RemoteControlHandlerContext);
16
- }
17
-
18
12
  export interface RemoteControlAdapter {
19
- handleRequest?: RemoteControlHandler;
20
13
  startServer?(options: { dataDir: string; handle: RemoteControlHandler }): void | (() => void | Promise<void>);
21
14
  registerHandler?(handler: RemoteControlHandler | null): void | (() => void);
22
15
  }
@@ -82,9 +75,5 @@ export function RemoteControlHost({
82
75
  };
83
76
  }, [adapter, controller]);
84
77
 
85
- return (
86
- <RemoteControlHandlerContext value={adapter?.handleRequest ?? (adapter ? controller.handle : null)}>
87
- {children}
88
- </RemoteControlHandlerContext>
89
- );
78
+ return children;
90
79
  }
@@ -9,7 +9,11 @@ import {
9
9
  } from "../../../../data/config/store";
10
10
  import type { AppConfig } from "../../../../types/config";
11
11
  import type { DesktopSharedStateSnapshot } from "../../../../types/desktop-window";
12
- import type { ReleaseInfo } from "../../../../updater";
12
+ import type {
13
+ DesktopBackendRequestPayload,
14
+ DesktopBackendRequestResponse,
15
+ DesktopCoreRequest,
16
+ } from "../../shared/protocol";
13
17
  import {
14
18
  checkElectrobunDesktopUpdate,
15
19
  } from "./update";
@@ -18,22 +22,6 @@ import {
18
22
  type DesktopWorkspace,
19
23
  } from "./workspace";
20
24
 
21
- const UNHANDLED_BACKEND_REQUEST = Symbol("unhandled backend request");
22
-
23
- interface DesktopBackendRequestHandled {
24
- handled: true;
25
- value: unknown;
26
- }
27
-
28
- interface DesktopBackendRequestUnhandled {
29
- handled: false;
30
- value: typeof UNHANDLED_BACKEND_REQUEST;
31
- }
32
-
33
- export type DesktopBackendRequestResult =
34
- | DesktopBackendRequestHandled
35
- | DesktopBackendRequestUnhandled;
36
-
37
25
  interface DesktopBackendRequestOptions {
38
26
  clearCurrentConfig: () => void;
39
27
  closeAllDetachedWindows: () => void;
@@ -42,8 +30,7 @@ interface DesktopBackendRequestOptions {
42
30
  getDesktopWorkspace: () => DesktopWorkspace | null;
43
31
  getServices: () => AppServices;
44
32
  getSessionSnapshot: () => AppSessionSnapshot | null;
45
- method: string;
46
- payload: Record<string, unknown>;
33
+ request: DesktopCoreRequest;
47
34
  reconcileDetachedWindows: () => void;
48
35
  registerCoreCapabilities: () => void;
49
36
  sendDesktopState: (snapshot: DesktopSharedStateSnapshot) => void;
@@ -55,19 +42,10 @@ interface DesktopBackendRequestOptions {
55
42
  teardownServices: () => void;
56
43
  }
57
44
 
58
- function handled(value: unknown): DesktopBackendRequestHandled {
59
- return { handled: true, value };
60
- }
61
-
62
- function unhandled(): DesktopBackendRequestUnhandled {
63
- return { handled: false, value: UNHANDLED_BACKEND_REQUEST };
64
- }
65
-
66
45
  async function importDesktopConfig({
67
46
  closeAllDetachedWindows,
68
47
  getConfig,
69
48
  getSessionSnapshot,
70
- payload,
71
49
  reconcileDetachedWindows,
72
50
  registerCoreCapabilities,
73
51
  sendDesktopState,
@@ -76,11 +54,13 @@ async function importDesktopConfig({
76
54
  setServices,
77
55
  syncConfigAccessors,
78
56
  teardownServices,
79
- }: DesktopBackendRequestOptions): Promise<AppConfig> {
57
+ }: DesktopBackendRequestOptions,
58
+ payload: DesktopBackendRequestPayload<"config.import">,
59
+ ): Promise<AppConfig> {
80
60
  closeAllDetachedWindows();
81
61
  setDesktopWorkspace(null);
82
62
  teardownServices();
83
- setCurrentConfig(await importConfig(payload.dataDir as string, payload.srcPath as string));
63
+ setCurrentConfig(await importConfig(payload.dataDir, payload.srcPath));
84
64
  setServices(createAppServices({
85
65
  config: getConfig(),
86
66
  plugins: getDesktopBackendPlugins(),
@@ -96,7 +76,7 @@ async function importDesktopConfig({
96
76
 
97
77
  export async function handleDesktopBackendRequest(
98
78
  options: DesktopBackendRequestOptions,
99
- ): Promise<DesktopBackendRequestResult> {
79
+ ): Promise<DesktopBackendRequestResponse<DesktopCoreRequest["method"]>> {
100
80
  const {
101
81
  clearCurrentConfig,
102
82
  closeAllDetachedWindows,
@@ -104,64 +84,68 @@ export async function handleDesktopBackendRequest(
104
84
  getConfig,
105
85
  getDesktopWorkspace,
106
86
  getServices,
107
- method,
108
- payload,
87
+ request,
109
88
  setCurrentConfig,
110
89
  setDesktopWorkspace,
111
90
  startUpdate,
112
91
  teardownServices,
113
92
  } = options;
114
93
 
115
- switch (method) {
94
+ switch (request.method) {
116
95
  case "update.check":
117
- return handled(checkElectrobunDesktopUpdate(
118
- typeof payload.currentVersion === "string" ? payload.currentVersion : "",
119
- ));
96
+ return checkElectrobunDesktopUpdate(
97
+ typeof request.payload.currentVersion === "string" ? request.payload.currentVersion : "",
98
+ );
120
99
  case "update.start": {
121
- const release = payload.release && typeof payload.release === "object"
122
- ? payload.release as Partial<ReleaseInfo>
123
- : null;
124
- startUpdate(typeof payload.currentVersion === "string" ? payload.currentVersion : release?.version ?? "");
125
- return handled(null);
100
+ const { release, currentVersion } = request.payload;
101
+ startUpdate(typeof currentVersion === "string" ? currentVersion : release.version);
102
+ return null;
126
103
  }
127
104
  case "ticker.loadAll":
128
- return handled(getServices().tickerRepository.loadAllTickers());
105
+ return getServices().tickerRepository.loadAllTickers();
129
106
  case "ticker.load":
130
- return handled(getServices().tickerRepository.loadTicker(payload.symbol as string));
107
+ return getServices().tickerRepository.loadTicker(request.payload.symbol);
131
108
  case "ticker.save":
132
- return handled(getServices().tickerRepository.saveTicker(payload.ticker as never));
109
+ await getServices().tickerRepository.saveTicker(request.payload.ticker);
110
+ return null;
133
111
  case "ticker.delete":
134
- return handled(getServices().tickerRepository.deleteTicker(payload.symbol as string));
112
+ await getServices().tickerRepository.deleteTicker(request.payload.symbol);
113
+ return null;
135
114
  case "config.save": {
136
- setCurrentConfig(payload.config as AppConfig);
115
+ setCurrentConfig(request.payload.config);
137
116
  const desktopWorkspace = getDesktopWorkspace();
138
117
  if (desktopWorkspace) {
139
118
  await commitDesktopSnapshot(desktopWorkspace.replaceConfig(getConfig(), { layoutChanged: true }));
140
- return handled(null);
119
+ return null;
141
120
  }
142
- return handled(saveConfig(getConfig()));
121
+ await saveConfig(getConfig());
122
+ return null;
143
123
  }
144
124
  case "config.resetAllData":
145
125
  closeAllDetachedWindows();
146
126
  setDesktopWorkspace(null);
147
127
  teardownServices();
148
128
  clearCurrentConfig();
149
- return handled(resetAllData(payload.dataDir as string));
129
+ await resetAllData(request.payload.dataDir);
130
+ return null;
150
131
  case "config.export":
151
- return handled(exportConfig(payload.config as AppConfig, payload.destPath as string));
132
+ await exportConfig(request.payload.config, request.payload.destPath);
133
+ return null;
152
134
  case "config.import":
153
- return handled(await importDesktopConfig(options));
135
+ return importDesktopConfig(options, request.payload);
154
136
  case "session.set":
155
137
  getServices().persistence.sessions.set(
156
- payload.sessionId as string,
157
- payload.value,
158
- payload.schemaVersion as number | undefined,
138
+ request.payload.sessionId,
139
+ request.payload.value,
140
+ request.payload.schemaVersion,
159
141
  );
160
- return handled(null);
142
+ return null;
161
143
  case "session.delete":
162
- getServices().persistence.sessions.delete(payload.sessionId as string);
163
- return handled(null);
164
- default:
165
- return unhandled();
144
+ getServices().persistence.sessions.delete(request.payload.sessionId);
145
+ return null;
146
+ default: {
147
+ const exhaustive: never = request;
148
+ throw new Error(`Unknown core backend method: ${String(exhaustive)}`);
149
+ }
166
150
  }
167
151
  }
@@ -1,4 +1,5 @@
1
1
  import type { AppServices } from "../../../../core/app-services";
2
+ import type { DesktopCapabilityRequest } from "../../shared/protocol";
2
3
  import { encodeRpcValue } from "../../view/rpc-codec";
3
4
 
4
5
  type CapabilityRegistry = AppServices["pluginRegistry"]["capabilities"];
@@ -45,26 +46,25 @@ export class DesktopCapabilityBridge<Rpc extends DesktopCapabilityRpc> {
45
46
 
46
47
  async handle(
47
48
  rpc: Rpc,
48
- method: string,
49
- payload: Record<string, unknown>,
49
+ request: DesktopCapabilityRequest,
50
50
  ): Promise<unknown> {
51
51
  const registry = this.options.getRegistry();
52
- switch (method) {
52
+ switch (request.method) {
53
53
  case "capability.invoke":
54
54
  return registry.invoke(
55
- payload.capabilityId as string,
56
- payload.operationId as string,
57
- payload.payload,
55
+ request.payload.capabilityId,
56
+ request.payload.operationId,
57
+ request.payload.payload,
58
58
  { renderer: true },
59
59
  );
60
60
  case "capability.subscribe": {
61
- const clientSubscriptionId = payload.subscriptionId as string;
61
+ const clientSubscriptionId = request.payload.subscriptionId;
62
62
  const scopedSubscriptionId = this.scopeClientId(rpc, clientSubscriptionId);
63
63
  this.subscriptions.get(scopedSubscriptionId)?.();
64
64
  await registry.subscribe(
65
- payload.capabilityId as string,
66
- payload.operationId as string,
67
- payload.payload,
65
+ request.payload.capabilityId,
66
+ request.payload.operationId,
67
+ request.payload.payload,
68
68
  (event) => {
69
69
  rpc.send["capability.event"]({
70
70
  subscriptionId: clientSubscriptionId,
@@ -77,13 +77,15 @@ export class DesktopCapabilityBridge<Rpc extends DesktopCapabilityRpc> {
77
77
  return null;
78
78
  }
79
79
  case "capability.unsubscribe": {
80
- const scopedSubscriptionId = this.scopeClientId(rpc, payload.subscriptionId as string);
80
+ const scopedSubscriptionId = this.scopeClientId(rpc, request.payload.subscriptionId);
81
81
  this.subscriptions.get(scopedSubscriptionId)?.();
82
82
  this.subscriptions.delete(scopedSubscriptionId);
83
83
  return null;
84
84
  }
85
- default:
86
- throw new Error(`Unknown capability method: ${method}`);
85
+ default: {
86
+ const exhaustive: never = request;
87
+ throw new Error(`Unknown capability method: ${String(exhaustive)}`);
88
+ }
87
89
  }
88
90
  }
89
91
 
@@ -1,10 +1,14 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { ContextMenu, Utils, type BrowserWindow } from "electrobun/bun";
3
3
  import { buildSoundCommand } from "../../../../notifications/app-notifier";
4
- import type { DesktopRestartMessage } from "../../shared/protocol";
4
+ import type {
5
+ DesktopBackendRequestResponse,
6
+ DesktopHostRequest,
7
+ DesktopRestartMessage,
8
+ DesktopWindowControlAction,
9
+ } from "../../shared/protocol";
5
10
  import { getContextMenuRequestId, normalizeContextMenuItems } from "../context-menu/normalize";
6
11
  import { MAIN_WINDOW_RPC_KEY } from "../window/focus";
7
- import type { DesktopWindowControlAction } from "./window-controls";
8
12
 
9
13
  interface DesktopHostRequestOptions<TRpc> {
10
14
  clearMainWindow: () => void;
@@ -13,8 +17,7 @@ interface DesktopHostRequestOptions<TRpc> {
13
17
  focusWindowForRpcKey: (windowKey: string) => void;
14
18
  getMainWindow: () => BrowserWindow | null;
15
19
  getRpcWindowKey: (rpc: TRpc) => string | undefined;
16
- method: string;
17
- payload: Record<string, unknown>;
20
+ request: DesktopHostRequest;
18
21
  restartDesktopApp: (message?: DesktopRestartMessage) => void;
19
22
  rpc: TRpc;
20
23
  teardownServices: () => void;
@@ -54,18 +57,17 @@ export function handleDesktopHostRequest<TRpc>({
54
57
  focusWindowForRpcKey,
55
58
  getMainWindow,
56
59
  getRpcWindowKey,
57
- method,
58
- payload,
60
+ request,
59
61
  restartDesktopApp,
60
62
  rpc,
61
63
  teardownServices,
62
64
  trackContextMenuRequest,
63
- }: DesktopHostRequestOptions<TRpc>): unknown {
64
- switch (method) {
65
+ }: DesktopHostRequestOptions<TRpc>): DesktopBackendRequestResponse<DesktopHostRequest["method"]> {
66
+ switch (request.method) {
65
67
  case "host.restart":
66
68
  restartDesktopApp({
67
- reason: typeof payload.reason === "string" ? payload.reason : undefined,
68
- source: typeof payload.source === "string" ? payload.source : "backend-request",
69
+ reason: typeof request.payload.reason === "string" ? request.payload.reason : undefined,
70
+ source: typeof request.payload.source === "string" ? request.payload.source : "backend-request",
69
71
  });
70
72
  return null;
71
73
  case "host.exit": {
@@ -81,7 +83,7 @@ export function handleDesktopHostRequest<TRpc>({
81
83
  return null;
82
84
  }
83
85
  case "host.windowControl": {
84
- const action = normalizeWindowControlAction(payload.action);
86
+ const action = normalizeWindowControlAction(request.payload.action);
85
87
  const windowKey = getRpcWindowKey(rpc);
86
88
  if (action === "close" && windowKey === MAIN_WINDOW_RPC_KEY) {
87
89
  closeAllDetachedWindows();
@@ -101,13 +103,13 @@ export function handleDesktopHostRequest<TRpc>({
101
103
  return null;
102
104
  }
103
105
  case "host.openExternal":
104
- if (typeof payload.url !== "string") {
106
+ if (typeof request.payload.url !== "string") {
105
107
  throw new Error("host.openExternal requires a URL.");
106
108
  }
107
- Utils.openExternal(payload.url);
109
+ Utils.openExternal(request.payload.url);
108
110
  return null;
109
111
  case "host.copyText":
110
- Utils.clipboardWriteText(normalizeText(payload.text) ?? "");
112
+ Utils.clipboardWriteText(normalizeText(request.payload.text) ?? "");
111
113
  return null;
112
114
  case "host.focusWindow": {
113
115
  const windowKey = getRpcWindowKey(rpc);
@@ -115,7 +117,7 @@ export function handleDesktopHostRequest<TRpc>({
115
117
  return null;
116
118
  }
117
119
  case "host.copyPngImage": {
118
- const pngBase64 = normalizeText(payload.pngBase64);
120
+ const pngBase64 = normalizeText(request.payload.pngBase64);
119
121
  if (!pngBase64) throw new Error("host.copyPngImage requires PNG data.");
120
122
  Utils.clipboardWriteImage(new Uint8Array(Buffer.from(pngBase64, "base64")));
121
123
  return null;
@@ -123,23 +125,25 @@ export function handleDesktopHostRequest<TRpc>({
123
125
  case "host.readText":
124
126
  return Utils.clipboardReadText() ?? "";
125
127
  case "host.notify":
126
- playNotificationSound(normalizeText(payload.sound));
128
+ playNotificationSound(normalizeText(request.payload.sound));
127
129
  Utils.showNotification({
128
- title: normalizeText(payload.title) ?? "Gloomberb",
129
- body: normalizeText(payload.body),
130
- subtitle: normalizeText(payload.subtitle),
130
+ title: normalizeText(request.payload.title) ?? "Gloomberb",
131
+ body: normalizeText(request.payload.body),
132
+ subtitle: normalizeText(request.payload.subtitle),
131
133
  silent: true,
132
134
  });
133
135
  return null;
134
136
  case "host.showContextMenu": {
135
- const menu = normalizeContextMenuItems(payload.menu);
137
+ const menu = normalizeContextMenuItems(request.payload.menu);
136
138
  if (menu.length === 0) return false;
137
139
  const requestId = getContextMenuRequestId(menu);
138
140
  if (requestId) trackContextMenuRequest(requestId, rpc);
139
141
  ContextMenu.showContextMenu(menu as never);
140
142
  return true;
141
143
  }
142
- default:
143
- throw new Error(`Unknown host method: ${method}`);
144
+ default: {
145
+ const exhaustive: never = request;
146
+ throw new Error(`Unknown host method: ${String(exhaustive)}`);
147
+ }
144
148
  }
145
149
  }