gloomberb 0.11.0 → 0.11.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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -0,0 +1,116 @@
1
+ interface StaticAssetsBinding {
2
+ fetch(request: Request): Promise<Response>;
3
+ }
4
+
5
+ export interface WorkerEnv {
6
+ ASSETS: StaticAssetsBinding;
7
+ }
8
+
9
+ const SHARE_PATH = /^\/s\/[a-f0-9]{32}\/?$/;
10
+ const LAYOUT_SHARE_PATH = /^\/l\/[a-f0-9]{32}\/?$/;
11
+ const API_PATH = /^\/api(?:\/|$)/;
12
+ const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
13
+
14
+ /**
15
+ * Lets the iOS app claim `https://term.gloom.sh/s/...` share links.
16
+ *
17
+ * Apple fetches this over HTTPS with its own client, so it has to be a direct
18
+ * 200 with JSON content type: a redirect, an HTML error page, or a 404 all
19
+ * silently disable universal links with no visible failure on device.
20
+ *
21
+ * The path list mirrors `SHARE_PATH` above. Only pane/content shares are
22
+ * claimed. Layout permalinks stay in the browser so their page can preview the
23
+ * workspace and hand it to the hosted terminal as an editable copy.
24
+ *
25
+ * `3XQML3UV65` is the Apple team id that prefixes the app id.
26
+ */
27
+ const APPLE_APP_SITE_ASSOCIATION = {
28
+ applinks: {
29
+ details: [
30
+ {
31
+ appIDs: ["3XQML3UV65.sh.gloom.companion"],
32
+ components: [{ "/": "/s/*", comment: "Public portfolio shares" }],
33
+ },
34
+ ],
35
+ },
36
+ } as const;
37
+ const API_ORIGIN = "https://api.gloom.sh";
38
+ const API_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
39
+ type ApiFetch = (request: Request) => Promise<Response>;
40
+
41
+ export const SECURITY_HEADERS = {
42
+ "content-security-policy": "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' https://api.github.com https://api.fiscaldata.treasury.gov; form-action 'self'; upgrade-insecure-requests",
43
+ "cross-origin-opener-policy": "same-origin",
44
+ "cross-origin-resource-policy": "same-origin",
45
+ "permissions-policy": "camera=(), geolocation=(), microphone=(), payment=(), usb=()",
46
+ "referrer-policy": "strict-origin-when-cross-origin",
47
+ "strict-transport-security": "max-age=31536000; includeSubDomains",
48
+ "x-content-type-options": "nosniff",
49
+ "x-frame-options": "DENY",
50
+ } as const;
51
+
52
+ export function withSecurityHeaders(response: Response, options: { share?: boolean } = {}): Response {
53
+ const headers = new Headers(response.headers);
54
+ for (const [name, value] of Object.entries(SECURITY_HEADERS)) headers.set(name, value);
55
+ if (options.share) headers.set("x-robots-tag", "noindex, nofollow, noarchive");
56
+ if ((headers.get("content-type") ?? "").includes("text/html")) {
57
+ headers.set("cache-control", "no-store");
58
+ }
59
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
60
+ }
61
+
62
+ async function proxyApi(request: Request, fetchApi: ApiFetch): Promise<Response> {
63
+ const url = new URL(request.url);
64
+ if (!API_METHODS.has(request.method)) {
65
+ return Response.json({ error: "Method not allowed" }, {
66
+ status: 405,
67
+ headers: { Allow: [...API_METHODS].join(", ") },
68
+ });
69
+ }
70
+ const origin = request.headers.get("origin");
71
+ if (origin && origin !== url.origin) {
72
+ return Response.json({ error: "Origin not allowed" }, { status: 403 });
73
+ }
74
+ const upstreamUrl = new URL(`${url.pathname.slice(4) || "/"}${url.search}`, API_ORIGIN);
75
+ return fetchApi(new Request(upstreamUrl, request));
76
+ }
77
+
78
+ export async function handleRequest(request: Request, env: WorkerEnv, fetchApi: ApiFetch = fetch): Promise<Response> {
79
+ const url = new URL(request.url);
80
+ if (API_PATH.test(url.pathname)) return proxyApi(request, fetchApi);
81
+
82
+ if (request.method !== "GET" && request.method !== "HEAD") {
83
+ return withSecurityHeaders(Response.json({ error: "Method not allowed" }, {
84
+ status: 405,
85
+ headers: { Allow: "GET, HEAD" },
86
+ }));
87
+ }
88
+
89
+ if (url.pathname === "/health") {
90
+ return withSecurityHeaders(Response.json({ status: "ok" }));
91
+ }
92
+
93
+ // Served before the asset handler so it cannot be shadowed by a static file
94
+ // or the SPA fallback, either of which would hand Apple HTML and quietly
95
+ // break universal links.
96
+ if (url.pathname === APPLE_APP_SITE_ASSOCIATION_PATH) {
97
+ return withSecurityHeaders(
98
+ Response.json(APPLE_APP_SITE_ASSOCIATION, {
99
+ headers: { "cache-control": "public, max-age=3600" },
100
+ }),
101
+ );
102
+ }
103
+ const socialShare = SHARE_PATH.test(url.pathname) || LAYOUT_SHARE_PATH.test(url.pathname);
104
+ if (socialShare) {
105
+ const assetUrl = new URL("/share.html", url.origin);
106
+ const assetRequest = new Request(assetUrl, { method: request.method, headers: request.headers });
107
+ return withSecurityHeaders(await env.ASSETS.fetch(assetRequest), { share: true });
108
+ }
109
+ return withSecurityHeaders(await env.ASSETS.fetch(request));
110
+ }
111
+
112
+ export default {
113
+ fetch(request: Request, env: WorkerEnv): Promise<Response> {
114
+ return handleRequest(request, env);
115
+ },
116
+ };
@@ -88,11 +88,12 @@ function buildApplicationMenu(): ApplicationMenuItemConfig[] {
88
88
  {
89
89
  label: "Layout",
90
90
  submenu: [
91
- openCommandBar("Layout Actions...", "LAY ", { accelerator: "CmdOrCtrl+Shift+L" }),
91
+ commandItem("Layouts...", { type: "open-layout-gallery" }, { accelerator: "CmdOrCtrl+Shift+L" }),
92
+ openCommandBar("Layout Actions...", "LMA "),
92
93
  { type: "divider" },
93
94
  commandItem("Undo Layout Change", { type: "layout-undo" }),
94
95
  commandItem("Redo Layout Change", { type: "layout-redo" }),
95
- commandItem("Gridlock All Windows", { type: "layout-gridlock" }, { accelerator: "CmdOrCtrl+Shift+G" }),
96
+ commandItem("Tidy Windows", { type: "layout-gridlock" }, { accelerator: "CmdOrCtrl+Shift+G" }),
96
97
  { type: "divider" },
97
98
  commandItem("New Layout...", { type: "open-plugin-workflow", commandId: "new-layout" }),
98
99
  commandItem("Rename Current Layout...", { type: "open-plugin-workflow", commandId: "rename-layout" }),
@@ -142,6 +142,7 @@ export async function initializeDesktopBackend<TRpc>(
142
142
  plugins: getDesktopBackendPlugins(),
143
143
  });
144
144
  options.setServices(services);
145
+ await services.ready;
145
146
  options.syncConfigAccessors();
146
147
  options.registerCoreCapabilities();
147
148
  options.setDesktopWorkspace(createDesktopWorkspace(config, options.getSessionSnapshot()));
@@ -76,9 +76,15 @@ export function handleDesktopPluginStateRequest(
76
76
  }
77
77
  }
78
78
 
79
+ function pluginStateIds(registry: PluginRegistry): string[] {
80
+ const store = registry.persistence.pluginState as { pluginIds?: () => string[] };
81
+ const storedIds = typeof store.pluginIds === "function" ? store.pluginIds() : [];
82
+ return [...new Set(["gloomberb-cloud", ...registry.allPlugins.keys(), ...storedIds])];
83
+ }
84
+
79
85
  export function loadDesktopPluginState(registry: PluginRegistry): Record<string, Record<string, unknown>> {
80
86
  const state: Record<string, Record<string, unknown>> = {};
81
- for (const pluginId of registry.allPlugins.keys()) {
87
+ for (const pluginId of pluginStateIds(registry)) {
82
88
  const keys = registry.persistence.pluginState.keys(pluginId);
83
89
  if (keys.length === 0) continue;
84
90
  state[pluginId] = {};
@@ -7,17 +7,18 @@ import type { AppRuntimeServices, AppServicesFactoryOptions } from "../../../cor
7
7
  import { newsProvider } from "../../../capabilities";
8
8
  import { debugLog } from "../../../utils/debug-log";
9
9
  import { measurePerf, measurePerfAsync } from "../../../utils/perf-marks";
10
- import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
11
10
  import { createRemoteAssetDataClient } from "./remote/asset-data-client";
12
11
  import { RemotePersistence } from "./remote/persistence";
13
12
  import { RemoteTickerRepository } from "./remote/ticker-repository";
14
13
  import { connectBackendConnectionHealth } from "./remote/connection-health-backend";
15
14
  import { backendRequest, getElectrobunBackendInitSnapshot } from "./backend-rpc";
16
15
  import { createCapabilityInvoker } from "./remote/capability-invoker";
16
+ import { apiClient } from "../../../api-client";
17
+ import { cloudNewsParams, mapCloudNewsArticle } from "../../../sources/gloomberb-cloud/news";
17
18
 
18
19
  const servicesLog = debugLog.createLogger("services");
19
20
 
20
- export function createElectrobunAppServices({ config }: AppServicesFactoryOptions): AppRuntimeServices {
21
+ export function createElectrobunAppServices({ config, plugins }: AppServicesFactoryOptions): AppRuntimeServices {
21
22
  servicesLog.info("create desktop web services start", {
22
23
  brokerInstanceCount: config.brokerInstances.length,
23
24
  });
@@ -54,10 +55,20 @@ export function createElectrobunAppServices({ config }: AppServicesFactoryOption
54
55
  priority: 0,
55
56
  provider: {
56
57
  fetchNews: (query) => dataProvider.getNews(query),
58
+ fetchNewsPage: async (query) => {
59
+ try {
60
+ const response = await apiClient.getCloudNews(cloudNewsParams(query));
61
+ return {
62
+ articles: response.items.map((item) => mapCloudNewsArticle(item, query.ticker)),
63
+ nextCursor: response.nextCursor ?? null,
64
+ };
65
+ } catch {
66
+ return { articles: await dataProvider.getNews(query), nextCursor: null };
67
+ }
68
+ },
57
69
  },
58
70
  }));
59
71
 
60
- const plugins = getRendererBuiltinPlugins();
61
72
  const pluginReadyPromises: Promise<void>[] = [];
62
73
  for (const plugin of plugins) {
63
74
  pluginReadyPromises.push(measurePerfAsync("startup.services.register-plugin", () => (
@@ -2,7 +2,7 @@ import { readFile, writeFile } from "fs/promises";
2
2
  import { join, relative } from "path";
3
3
  import { TITLEBAR_OVERLAY_HEIGHT_PX } from "../../../components/layout/titlebar-overlay";
4
4
 
5
- type AliasRule = readonly [string, string] | readonly [string, string, string];
5
+ export type AliasRule = readonly [string, string] | readonly [string, string, string];
6
6
  type PageOptions = {
7
7
  entrypoint: string;
8
8
  outdir: string;
@@ -61,15 +61,7 @@ async function buildElectrobunViewBundle({
61
61
  // already reads from the environment is baked in at build time.
62
62
  __GLOOMBERB_API_URL__: JSON.stringify(process.env.GLOOMBERB_API_URL ?? ""),
63
63
  },
64
- plugins: [
65
- {
66
- name: pluginName,
67
- setup(build) {
68
- const aliasRules = [...extraAliasRules, ...COMMON_ALIAS_RULES];
69
- build.onResolve({ filter: /.*/ }, (args) => resolveAlias(args, aliasRules));
70
- },
71
- },
72
- ],
64
+ plugins: [electrobunViewAliasPlugin(pluginName, extraAliasRules)],
73
65
  });
74
66
 
75
67
  if (!result.success) {
@@ -113,6 +105,16 @@ ${bootstrapScript}
113
105
  `;
114
106
  }
115
107
 
108
+ export function electrobunViewAliasPlugin(name: string, extraAliasRules: AliasRule[] = []) {
109
+ return {
110
+ name,
111
+ setup(build: { onResolve(options: { filter: RegExp }, callback: (args: { path: string; importer?: string }) => unknown): void }) {
112
+ const aliasRules = [...extraAliasRules, ...COMMON_ALIAS_RULES];
113
+ build.onResolve({ filter: /.*/ }, (args) => resolveAlias(args, aliasRules));
114
+ },
115
+ };
116
+ }
117
+
116
118
  function resolveAlias(args: { path: string; importer?: string }, aliasRules: AliasRule[]) {
117
119
  const importer = args.importer?.replaceAll("\\", "/");
118
120
  for (const rule of aliasRules) {
@@ -546,7 +546,7 @@ function ShotPane({ payload }: { payload: CliPaneShotPayload }) {
546
546
  config: payload.config,
547
547
  paneState: payload.paneState,
548
548
  } as Pick<AppState, "config" | "paneState">;
549
- const title = getPaneDisplayTitle(titleState, instance, pane);
549
+ const title = getPaneDisplayTitle(titleState, instance, pane, shotRegistry?.panes);
550
550
  const width = payload.widthCells;
551
551
  const height = payload.heightCells;
552
552
  const bodyFrame = resolvePaneBodyFrame({
@@ -140,6 +140,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
140
140
  }, [emitVisibleRange, onBodyScrollActivity]);
141
141
  const scheduleBodyScrollActivity = useRafCallback(handleBodyScrollActivity);
142
142
  const scheduleVisibleRangeMeasure = useRafCallback(emitVisibleRange);
143
+ const lastAppliedScrollRequestRef = useRef<string | null>(null);
143
144
 
144
145
  const rowVirtualizer = useVirtualizer({
145
146
  count: items.length,
@@ -203,7 +204,12 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
203
204
  // then converted to pixels. Letting the virtualizer scroll by pixels landed
204
205
  // mid-row and clipped the first and last visible rows into slivers.
205
206
  useEffect(() => {
206
- if (scrollToIndex == null || items.length === 0) return;
207
+ if (scrollToIndex == null || items.length === 0) {
208
+ lastAppliedScrollRequestRef.current = null;
209
+ return;
210
+ }
211
+ const scrollRequestKey = `${scrollToIndex}:${scrollToIndexVersion}:${scrollToIndexAlign}`;
212
+ if (lastAppliedScrollRequestRef.current === scrollRequestKey) return;
207
213
  const targetIndex = Math.max(0, Math.min(scrollToIndex, items.length - 1));
208
214
  const element = bodyElementRef.current;
209
215
  if (!element) return;
@@ -219,13 +225,16 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
219
225
  }
220
226
  if (nextTop !== currentTop) {
221
227
  element.scrollTop = nextTop * WEB_CELL_HEIGHT;
228
+ scheduleBodyScrollActivity();
222
229
  }
230
+ lastAppliedScrollRequestRef.current = scrollRequestKey;
223
231
  scheduleVisibleRangeMeasure();
224
232
  }, [
225
233
  items.length,
226
234
  scrollToIndex,
227
235
  scrollToIndexAlign,
228
236
  scrollToIndexVersion,
237
+ scheduleBodyScrollActivity,
229
238
  scheduleVisibleRangeMeasure,
230
239
  ]);
231
240
 
@@ -216,6 +216,7 @@ export function WebTextField({
216
216
  onBlur,
217
217
  hint,
218
218
  type = "text",
219
+ autoComplete,
219
220
  variant = "default",
220
221
  backgroundColor,
221
222
  textColor,
@@ -273,6 +274,7 @@ export function WebTextField({
273
274
  width="100%"
274
275
  value={value}
275
276
  type={type}
277
+ autoComplete={autoComplete}
276
278
  placeholder={placeholder}
277
279
  focused={focused}
278
280
  textColor={resolvedTextColor}
@@ -0,0 +1,143 @@
1
+ /** @jsxImportSource react */
2
+ import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
3
+ import type { UiHost } from "../../../ui/host";
4
+ import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "./input-host";
5
+ import { WebDataTable } from "./data-table";
6
+ import {
7
+ WebButton,
8
+ WebCheckbox,
9
+ WebDialogFrame,
10
+ WebListView,
11
+ WebMessageComposer,
12
+ WebPageStackView,
13
+ WebSegmentedControl,
14
+ WebTextField,
15
+ } from "./desktop/controls";
16
+ import { WebPopover } from "./desktop/popover";
17
+ import { WebBox } from "./host/box";
18
+ import { WebChartSurface } from "./host/chart-surface";
19
+ import { WebInput, WebTextarea } from "./host/input";
20
+ import { WebMediaSurface } from "./host/media-surface";
21
+ import { WebScrollBox } from "./host/scroll-box";
22
+ import { cleanDomProps, commonStyle } from "./host/style";
23
+ import { WebAsciiText, WebSpan, WebStrong, WebText, WebUnderline } from "./host/text";
24
+ import { WebTabs } from "./host/tabs";
25
+
26
+ function currentDesktopPlatform(): string {
27
+ const navigatorWithUserAgentData = navigator as Navigator & {
28
+ userAgentData?: { platform?: string };
29
+ };
30
+ return [
31
+ navigatorWithUserAgentData.userAgentData?.platform,
32
+ navigator.platform,
33
+ navigator.userAgent,
34
+ ].filter((value): value is string => Boolean(value)).join(" ");
35
+ }
36
+
37
+ const DESKTOP_PLATFORM = currentDesktopPlatform();
38
+ const USES_WINDOWS_CONTROLS = !/(darwin|ipad|iphone|linux|mac)/i.test(DESKTOP_PLATFORM);
39
+ const NON_WINDOWS_DESKTOP_PLATFORMS = /^(darwin|linux|freebsd|openbsd|aix|sunos)$/i;
40
+
41
+ function usesWindowsWindowControls(desktopPlatform?: string): boolean {
42
+ const platform = desktopPlatform?.trim();
43
+ if (!platform) return USES_WINDOWS_CONTROLS;
44
+ if (/^win/i.test(platform)) return true;
45
+ if (NON_WINDOWS_DESKTOP_PLATFORMS.test(platform)) return false;
46
+ return USES_WINDOWS_CONTROLS;
47
+ }
48
+
49
+ export function createDomUiHost(
50
+ desktopPlatform?: string,
51
+ options: {
52
+ nativeContextMenu?: boolean;
53
+ titleBarOverlay?: boolean;
54
+ nativePaneChrome?: boolean;
55
+ nativeWindowChrome?: boolean;
56
+ publicSharing?: boolean;
57
+ } = {},
58
+ ): UiHost {
59
+ const usesWindowsControls = options.titleBarOverlay !== false
60
+ && options.nativeWindowChrome !== false
61
+ && usesWindowsWindowControls(desktopPlatform);
62
+ return {
63
+ kind: "desktop-web",
64
+ capabilities: {
65
+ nativePaneChrome: options.nativePaneChrome ?? true,
66
+ titleBarOverlay: options.titleBarOverlay ?? true,
67
+ nativeWindowChrome: options.nativeWindowChrome ?? true,
68
+ publicSharing: options.publicSharing ?? false,
69
+ precisePointer: true,
70
+ fractionalViewport: true,
71
+ cellWidthPx: WEB_CELL_WIDTH,
72
+ cellHeightPx: WEB_CELL_HEIGHT,
73
+ pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
74
+ canvasCharts: true,
75
+ nativeContextMenu: options.nativeContextMenu ?? false,
76
+ windowControls: usesWindowsControls ? "windows" : undefined,
77
+ },
78
+ Box: WebBox,
79
+ Text: WebText,
80
+ Span: WebSpan,
81
+ Strong: WebStrong,
82
+ Underline: WebUnderline,
83
+ ScrollBox: WebScrollBox,
84
+ Input: WebInput,
85
+ Textarea: WebTextarea,
86
+ Button: WebButton,
87
+ Checkbox: WebCheckbox,
88
+ Popover: WebPopover,
89
+ TextField: WebTextField,
90
+ MessageComposer: WebMessageComposer,
91
+ ListView: WebListView,
92
+ SegmentedControl: WebSegmentedControl,
93
+ DialogFrame: WebDialogFrame,
94
+ PageStackView: WebPageStackView,
95
+ DataTable: WebDataTable,
96
+ Tabs: WebTabs,
97
+ ChartSurface: WebChartSurface,
98
+ ImageSurface: ({ children, src, alt = "", objectFit = "contain", ...props }) => {
99
+ const imageSrc = typeof src === "string" ? src.trim() : "";
100
+ const [failed, setFailed] = useState(false);
101
+ useEffect(() => setFailed(false), [imageSrc]);
102
+ const baseStyle = commonStyle(props);
103
+ return (
104
+ <div
105
+ {...cleanDomProps(props)}
106
+ style={{
107
+ ...baseStyle,
108
+ overflow: baseStyle.overflow ?? "hidden",
109
+ ...(props.style as CSSProperties | undefined),
110
+ }}
111
+ >
112
+ {imageSrc && !failed ? (
113
+ <img
114
+ key={imageSrc}
115
+ src={imageSrc}
116
+ alt={alt}
117
+ draggable={false}
118
+ onError={() => setFailed(true)}
119
+ style={{ width: "100%", height: "100%", display: "block", objectFit }}
120
+ />
121
+ ) : children as ReactNode}
122
+ </div>
123
+ );
124
+ },
125
+ MediaSurface: WebMediaSurface,
126
+ SpinnerMark: ({ color, ...props }) => (
127
+ <span
128
+ {...cleanDomProps(props)}
129
+ aria-hidden="true"
130
+ style={{
131
+ color,
132
+ display: "inline-block",
133
+ width: "1ch",
134
+ animation: "gloom-spin 0.9s steps(8) infinite",
135
+ ...(props.style as CSSProperties | undefined),
136
+ }}
137
+ >
138
+ *
139
+ </span>
140
+ ),
141
+ AsciiText: (props) => <WebAsciiText {...props} desktopPlatform={desktopPlatform} />,
142
+ };
143
+ }
@@ -71,6 +71,39 @@ test("reports a drag as a drag, never as a move, and keeps its modifiers", async
71
71
  expect(modifiers.every((entry) => entry.shift)).toBe(true);
72
72
  });
73
73
 
74
+ test("chart surfaces consume browser pan and zoom gestures", async () => {
75
+ const directions: string[] = [];
76
+ const container = testWindow.document.createElement("div");
77
+ testWindow.document.body.appendChild(container);
78
+ const root = createRoot(container as unknown as HTMLElement);
79
+ await act(async () => {
80
+ root.render(
81
+ <WebChartSurface
82
+ width={40}
83
+ height={10}
84
+ onMouseScroll={(event: { scroll?: { direction?: string }; preventDefault: () => void }) => {
85
+ directions.push(event.scroll?.direction ?? "");
86
+ event.preventDefault();
87
+ }}
88
+ />,
89
+ );
90
+ });
91
+
92
+ const surface = container.firstElementChild as unknown as HTMLElement;
93
+ const wheel = new testWindow.WheelEvent("wheel", {
94
+ bubbles: true,
95
+ cancelable: true,
96
+ deltaX: 24,
97
+ });
98
+ expect(surface.dispatchEvent(wheel as never)).toBe(false);
99
+ expect(wheel.defaultPrevented).toBe(true);
100
+ expect(directions).toEqual(["right"]);
101
+ expect(surface.style.touchAction).toBe("none");
102
+ expect(surface.style.overscrollBehavior).toBe("none");
103
+
104
+ await act(async () => root.unmount());
105
+ });
106
+
74
107
  test("a tab bar occupies exactly the one row panes reserve for it", async () => {
75
108
  const { WebTabs } = await import("./tabs");
76
109
  const { WEB_CELL_HEIGHT } = await import("../input-host");
@@ -8,7 +8,6 @@ import {
8
8
  type MouseEvent,
9
9
  type ReactNode,
10
10
  type Ref,
11
- type WheelEvent as ReactWheelEvent,
12
11
  } from "react";
13
12
  import {
14
13
  type CellMouseEvent,
@@ -96,6 +95,17 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
96
95
  document.body.classList.remove("gloom-dragging");
97
96
  }, []);
98
97
 
98
+ const handlesWheel = typeof props.onMouseScroll === "function";
99
+ useEffect(() => {
100
+ const element = elementRef.current;
101
+ if (!element || !handlesWheel) return;
102
+ const handleWheel = (event: WheelEvent) => {
103
+ callMouseHandler(propsRef.current.onMouseScroll, event, "scroll");
104
+ };
105
+ element.addEventListener("wheel", handleWheel, { passive: false });
106
+ return () => element.removeEventListener("wheel", handleWheel);
107
+ }, [handlesWheel]);
108
+
99
109
  const stopDocumentDrag = () => {
100
110
  if (!draggingRef.current) return;
101
111
  draggingRef.current = false;
@@ -136,10 +146,6 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
136
146
  callMouseHandler(propsRef.current.onMouseDownCapture, event, "down");
137
147
  };
138
148
 
139
- const handleWheel = (event: ReactWheelEvent<HTMLDivElement>) => {
140
- callMouseHandler(propsRef.current.onMouseScroll, event, "scroll");
141
- };
142
-
143
149
  const handleMouseOver = (event: MouseEvent) => {
144
150
  callMouseHandler(propsRef.current.onMouseOver, event, "over");
145
151
  };
@@ -160,7 +166,6 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
160
166
  onMouseMove={(event) => scheduleFrameMouseHandler(event, "move")}
161
167
  onMouseUp={(event) => callMouseHandler(propsRef.current.onMouseUp, event, "up")}
162
168
  onMouseOut={handleMouseOut}
163
- onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
164
169
  style={{
165
170
  ...commonStyle(props),
166
171
  "--gloom-box-hover-bg": hoverBackgroundColor,
@@ -211,7 +211,13 @@ export const WebChartSurface = forwardRef<BoxRenderable, Record<string, unknown>
211
211
  {...props}
212
212
  ref={ref as Ref<HTMLDivElement>}
213
213
  data-gloom-role={(props["data-gloom-role"] as string | undefined) ?? "chart-surface"}
214
- style={{ position: "relative", overflow: "hidden", ...(props.style as CSSProperties | undefined) }}
214
+ style={{
215
+ position: "relative",
216
+ overflow: "hidden",
217
+ touchAction: "none",
218
+ overscrollBehavior: "none",
219
+ ...(props.style as CSSProperties | undefined),
220
+ }}
215
221
  >
216
222
  {layers.length > 0
217
223
  ? layers.map((layer, index) => (
@@ -10,9 +10,9 @@ import {
10
10
  type KeyboardEvent,
11
11
  type RefObject,
12
12
  } from "react";
13
- import type { InputRenderable, TextareaRenderable } from "../../../../ui/host";
13
+ import { editableTextContextMenuItems } from "../../../../ui/context-menu";
14
+ import { useRendererHost, useUiCapabilities, type InputRenderable, type TextareaRenderable } from "../../../../ui/host";
14
15
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../input-host";
15
- import { NATIVE_CONTEXT_MENU_SUPPORTED, showEditableTextContextMenu } from "./native";
16
16
  import { cellHeight, cellWidth, cleanDomProps, commonStyle } from "./style";
17
17
 
18
18
  function textInputStyle(props: Record<string, unknown>, multiline: boolean): CSSProperties {
@@ -205,6 +205,8 @@ function textareaMetrics(
205
205
  }
206
206
 
207
207
  export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(function WebInput(props, ref) {
208
+ const renderer = useRendererHost();
209
+ const { nativeContextMenu } = useUiCapabilities();
208
210
  const elementRef = useRef<HTMLInputElement | null>(null);
209
211
  const propsRef = useLatestRef(props);
210
212
  const { value, valueRef, setValue } = useEditableValue(props);
@@ -271,7 +273,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
271
273
  value={value}
272
274
  autoCorrect="off"
273
275
  autoCapitalize="off"
274
- autoComplete="off"
276
+ autoComplete={getStringProp(props, "autoComplete") ?? "off"}
275
277
  spellCheck={false}
276
278
  placeholder={getStringProp(props, "placeholder")}
277
279
  onInput={(event) => handleValueChange(event.currentTarget.value)}
@@ -287,11 +289,11 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
287
289
  }}
288
290
  onKeyDown={handleKeyDown}
289
291
  onContextMenu={(event) => {
290
- if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
292
+ if (!nativeContextMenu || !renderer.showContextMenu) return;
291
293
  elementRef.current?.focus();
292
294
  event.preventDefault();
293
295
  event.stopPropagation();
294
- void showEditableTextContextMenu();
296
+ void renderer.showContextMenu(editableTextContextMenuItems());
295
297
  }}
296
298
  onSelect={() => {
297
299
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
@@ -303,6 +305,8 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
303
305
  });
304
306
 
305
307
  export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown>>(function WebTextarea(props, ref) {
308
+ const renderer = useRendererHost();
309
+ const { nativeContextMenu } = useUiCapabilities();
306
310
  const elementRef = useRef<HTMLTextAreaElement | null>(null);
307
311
  const propsRef = useLatestRef(props);
308
312
  const { value, valueRef, setValue } = useEditableValue(props);
@@ -400,11 +404,11 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
400
404
  }}
401
405
  onKeyDown={handleKeyDown}
402
406
  onContextMenu={(event) => {
403
- if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
407
+ if (!nativeContextMenu || !renderer.showContextMenu) return;
404
408
  elementRef.current?.focus();
405
409
  event.preventDefault();
406
410
  event.stopPropagation();
407
- void showEditableTextContextMenu();
411
+ void renderer.showContextMenu(editableTextContextMenuItems());
408
412
  }}
409
413
  onSelect={() => {
410
414
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
@@ -1,5 +1,4 @@
1
1
  import type { ContextMenuItem } from "../../../../types/context-menu";
2
- import { editableTextContextMenuItems } from "../../../../ui/context-menu";
3
2
  import { backendRequest, onContextMenuSelect } from "../backend-rpc";
4
3
  import {
5
4
  DesktopContextMenuActionScope,
@@ -42,7 +41,3 @@ export async function showDesktopContextMenu(items: ContextMenuItem[]): Promise<
42
41
  return false;
43
42
  }
44
43
  }
45
-
46
- export function showEditableTextContextMenu(): Promise<boolean> {
47
- return showDesktopContextMenu(editableTextContextMenuItems());
48
- }
@@ -30,6 +30,7 @@ import { createDesktopDeepLinkBridge } from "./desktop-deeplink-bridge";
30
30
  import { createDesktopWindowBridge } from "./desktop/window/bridge";
31
31
  import { prepareDetachedSnapshot } from "./desktop/window/snapshot";
32
32
  import { createElectrobunAppServices } from "./app-services";
33
+ import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
33
34
 
34
35
  const rootElement = document.getElementById("root");
35
36
  if (!rootElement) {
@@ -112,6 +113,7 @@ async function boot() {
112
113
  <App
113
114
  config={config}
114
115
  servicesFactory={createElectrobunAppServices}
116
+ plugins={getRendererBuiltinPlugins()}
115
117
  desktopWindowBridge={desktopWindowBridge}
116
118
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
117
119
  desktopDeepLinkBridge={desktopDeepLinkBridge}
@@ -77,6 +77,11 @@ class RemotePluginStateStore {
77
77
  if (!this.state.has(pluginId)) this.state.set(pluginId, new Map());
78
78
  this.state.get(pluginId)!.set(key, value);
79
79
  this.getScheduler(pluginId, key).schedule({ pluginId, key, value, schemaVersion });
80
+ // Closing the window tears down the RPC before a debounced save can land.
81
+ // Auth has to reach SQLite immediately or the next launch is signed out.
82
+ if (key === "session" || key === "resume:session") {
83
+ void this.flush();
84
+ }
80
85
  }
81
86
 
82
87
  delete(pluginId: string, key: string): void {