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,117 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createInitialState } from "../../state/app/context";
3
+ import { createTestDataProvider } from "../../test-support/data-provider";
4
+ import { createDefaultConfig, type PaneInstanceConfig } from "../../types/config";
5
+ import type { PaneDef } from "../../types/plugin";
6
+ import { bindAppPanePluginRegistry } from "./plugin-bindings";
7
+
8
+ const paneDef: PaneDef = {
9
+ id: "prediction-markets",
10
+ name: "Prediction Markets",
11
+ component: () => null,
12
+ defaultPosition: "left",
13
+ defaultMode: "floating",
14
+ };
15
+
16
+ function bindPortablePaneRuntime(disabledPlugins: string[] = []) {
17
+ const config = createDefaultConfig("/tmp/gloomberb-portable-pane-test");
18
+ config.disabledPlugins = disabledPlugins;
19
+ const state = createInitialState(config);
20
+ const actions: any[] = [];
21
+ const built: PaneInstanceConfig[] = [];
22
+ const placed: Array<{ instance: PaneInstanceConfig; options: unknown }> = [];
23
+ const pluginRegistry = {
24
+ panes: new Map([[paneDef.id, paneDef]]),
25
+ getPanePluginId: () => "prediction-markets",
26
+ getTermSizeFn: () => ({ width: 120, height: 40 }),
27
+ } as any;
28
+
29
+ bindAppPanePluginRegistry({
30
+ activatePane() {},
31
+ buildPaneInstance: (paneId, options) => {
32
+ const instance = { paneId, ...options } as PaneInstanceConfig;
33
+ built.push(instance);
34
+ return instance;
35
+ },
36
+ createPaneFromTemplate: async () => {},
37
+ dataProvider: createTestDataProvider(),
38
+ detachedPaneId: null,
39
+ dispatch: (action) => actions.push(action),
40
+ focusVisiblePane() {},
41
+ isDetachedWindow: false,
42
+ openPaneSettings: async () => {},
43
+ openPinnedTicker: async () => {},
44
+ persistLayout() {},
45
+ placePaneInstance: (instance, _def, options) => placed.push({ instance, options }),
46
+ placePinnedTickerTarget() {},
47
+ pluginRegistry,
48
+ publishTickerOpenTarget() {},
49
+ resolveOpenTickerTarget: async () => null,
50
+ resolvePaneTarget: () => null,
51
+ selectTickerInPane() {},
52
+ showPane() {},
53
+ state,
54
+ stateRef: { current: state },
55
+ switchTickerResearchTab() {},
56
+ tickerRepository: {} as any,
57
+ });
58
+
59
+ return { actions, built, placed, pluginRegistry };
60
+ }
61
+
62
+ const portablePane = {
63
+ schemaVersion: 2 as const,
64
+ sourceConfigVersion: 13,
65
+ layout: {
66
+ dockRoot: null,
67
+ instances: [{
68
+ instanceId: "p1",
69
+ paneId: "prediction-markets",
70
+ title: "Fed Markets",
71
+ binding: { kind: "none" as const },
72
+ params: { query: "fed" },
73
+ settings: { dense: true },
74
+ }],
75
+ floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
76
+ detached: [],
77
+ },
78
+ paneState: {
79
+ p1: { pluginState: { "prediction-markets": { searchQuery: "fed" } } },
80
+ },
81
+ };
82
+
83
+ describe("portable pane runtime", () => {
84
+ test("creates a fresh floating pane and restores its complete state", async () => {
85
+ const runtime = bindPortablePaneRuntime();
86
+
87
+ await runtime.pluginRegistry.openPortablePaneShareAsyncFn(portablePane);
88
+
89
+ expect(runtime.built).toHaveLength(1);
90
+ expect(runtime.built[0]).toMatchObject({
91
+ paneId: "prediction-markets",
92
+ title: "Fed Markets",
93
+ binding: { kind: "none" },
94
+ params: { query: "fed" },
95
+ settings: { dense: true },
96
+ });
97
+ expect(runtime.built[0]!.instanceId).toMatch(/^prediction-markets:shared-/);
98
+ expect(runtime.placed).toEqual([{
99
+ instance: runtime.built[0],
100
+ options: { placement: "floating" },
101
+ }]);
102
+ expect(runtime.actions).toContainEqual({
103
+ type: "REPLACE_PANE_STATE",
104
+ paneId: runtime.built[0]!.instanceId,
105
+ state: portablePane.paneState.p1,
106
+ });
107
+ expect(runtime.actions.some((action) => action.type === "INSTALL_LAYOUT_COPY")).toBe(false);
108
+ });
109
+
110
+ test("rejects panes owned by a disabled plugin", async () => {
111
+ const runtime = bindPortablePaneRuntime(["prediction-markets"]);
112
+ await expect(runtime.pluginRegistry.openPortablePaneShareAsyncFn(portablePane)).rejects.toThrow(
113
+ "unavailable",
114
+ );
115
+ expect(runtime.built).toHaveLength(0);
116
+ });
117
+ });
@@ -5,6 +5,7 @@ import {
5
5
  } from "../../components/command-bar/workflow/ops";
6
6
  import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
7
7
  import { setLayoutManagerDispatch } from "../../plugins/builtin/layout-manager";
8
+ import { materializeMarketplaceLayout } from "../../layout-marketplace/payload";
8
9
  import {
9
10
  isPaneInLayout,
10
11
  removePane,
@@ -112,7 +113,7 @@ export function bindAppPanePluginRegistry({
112
113
  launch: { kind: "plugin-command", commandId },
113
114
  });
114
115
  };
115
- pluginRegistry.getLayoutFn = () => state.config.layout;
116
+ pluginRegistry.getLayoutFn = () => stateRef.current.config.layout;
116
117
  pluginRegistry.updateLayoutFn = (layout) => {
117
118
  if (isDetachedWindow) return;
118
119
  persistLayout(layout);
@@ -134,6 +135,25 @@ export function bindAppPanePluginRegistry({
134
135
  placePaneInstance,
135
136
  });
136
137
  };
138
+ pluginRegistry.openPortablePaneShareAsyncFn = async (payload) => {
139
+ if (isDetachedWindow) throw new Error("Open shared panes in the main window.");
140
+ const materialized = materializeMarketplaceLayout(payload);
141
+ const sharedPane = materialized.layout.instances[0];
142
+ if (!sharedPane) throw new Error("This shared pane is invalid.");
143
+ const paneDef = pluginRegistry.panes.get(sharedPane.paneId);
144
+ const ownerId = pluginRegistry.getPanePluginId(sharedPane.paneId);
145
+ if (!paneDef || (ownerId && stateRef.current.config.disabledPlugins.includes(ownerId))) {
146
+ throw new Error("This shared pane is unavailable in this version of Gloomberb.");
147
+ }
148
+ const instance = buildPaneInstance(sharedPane.paneId, sharedPane);
149
+ if (!instance) throw new Error("This shared pane could not be created.");
150
+ dispatch({
151
+ type: "REPLACE_PANE_STATE",
152
+ paneId: instance.instanceId,
153
+ state: materialized.paneState[instance.instanceId] ?? {},
154
+ });
155
+ placePaneInstance(instance, paneDef, { placement: "floating" });
156
+ };
137
157
  pluginRegistry.createPaneFromTemplateFn = (templateId, options) => {
138
158
  if (isDetachedWindow) return;
139
159
  void createPaneFromTemplate(templateId, options);
@@ -151,8 +171,9 @@ export function bindAppPanePluginRegistry({
151
171
  pluginRegistry.hidePaneFn = (paneId) => {
152
172
  if (isDetachedWindow) return;
153
173
  const instanceId = resolvePaneTarget(paneId);
154
- if (!instanceId || !isPaneInLayout(state.config.layout, instanceId)) return;
155
- persistLayout(removePane(state.config.layout, instanceId));
174
+ const layout = stateRef.current.config.layout;
175
+ if (!instanceId || !isPaneInLayout(layout, instanceId)) return;
176
+ persistLayout(removePane(layout, instanceId));
156
177
  };
157
178
  pluginRegistry.focusPaneFn = (paneId) => {
158
179
  if (isDetachedWindow) {
@@ -162,7 +183,7 @@ export function bindAppPanePluginRegistry({
162
183
  return;
163
184
  }
164
185
  const instanceId = resolvePaneTarget(paneId);
165
- if (!instanceId || !isPaneInLayout(state.config.layout, instanceId)) {
186
+ if (!instanceId || !isPaneInLayout(stateRef.current.config.layout, instanceId)) {
166
187
  showPane(paneId);
167
188
  return;
168
189
  }
@@ -3,9 +3,12 @@ import {
3
3
  addPaneFloating,
4
4
  addPaneToLayout,
5
5
  findDockLeaf,
6
- isPaneInLayout,
7
6
  } from "../../plugins/pane-manager";
8
7
  import type { PluginRegistry } from "../../plugins/registry";
8
+ import {
9
+ findTickerResearchFollower,
10
+ listVisibleTickerSourcePanes,
11
+ } from "../../plugins/ticker-navigation";
9
12
  import type {
10
13
  AppAction,
11
14
  AppState,
@@ -45,6 +48,7 @@ export function useAppTickerInspectorRuntime({
45
48
  pluginRegistry,
46
49
  state,
47
50
  }: UseAppTickerInspectorRuntimeOptions) {
51
+ // Only for APIs that must land in a collection (selecting a symbol inside a list).
48
52
  const resolveCollectionSourcePaneId = useCallback((preferredPaneId?: string | null) => {
49
53
  return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isCollectionPaneInstance)?.instanceId
50
54
  ?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isCollectionPaneInstance)?.instanceId
@@ -52,6 +56,18 @@ export function useAppTickerInspectorRuntime({
52
56
  ?? null;
53
57
  }, [state.config.layout, state.focusedPaneId]);
54
58
 
59
+ /** Any visible pane that publishes a cursor symbol can drive a follow binding. */
60
+ const resolveTickerSourcePaneId = useCallback((preferredPaneId?: string | null) => {
61
+ const isTickerSource = (instance: PaneInstanceConfig) => (
62
+ pluginRegistry.panes.get(instance.paneId)?.tickerSource === true
63
+ );
64
+ return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isTickerSource)?.instanceId
65
+ ?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isTickerSource)?.instanceId
66
+ ?? findPrimaryPaneInstance(state.config.layout, "portfolio-list")?.instanceId
67
+ ?? listVisibleTickerSourcePanes(state.config.layout, pluginRegistry.panes)[0]?.instanceId
68
+ ?? null;
69
+ }, [pluginRegistry, state.config.layout, state.focusedPaneId]);
70
+
55
71
  const resolveTickerContextSourcePaneId = useCallback((preferredPaneId?: string | null) => {
56
72
  return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isTickerContextPaneInstance)?.instanceId
57
73
  ?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isTickerContextPaneInstance)?.instanceId
@@ -67,12 +83,7 @@ export function useAppTickerInspectorRuntime({
67
83
  }, [dispatch, resolveCollectionSourcePaneId]);
68
84
 
69
85
  const resolveInspectorPane = useCallback((sourcePaneId: string): PaneInstanceConfig | null => {
70
- return state.config.layout.instances.find((instance) =>
71
- instance.paneId === TICKER_RESEARCH_PANE_ID
72
- && instance.binding?.kind === "follow"
73
- && instance.binding.sourceInstanceId === sourcePaneId
74
- && isPaneInLayout(state.config.layout, instance.instanceId),
75
- ) ?? null;
86
+ return findTickerResearchFollower(state.config.layout, sourcePaneId);
76
87
  }, [state.config.layout]);
77
88
 
78
89
  const ensureInspectorPane = useCallback((sourcePaneId: string) => {
@@ -104,7 +115,7 @@ export function useAppTickerInspectorRuntime({
104
115
  if (target?.paneId === TICKER_RESEARCH_PANE_ID) return target.instanceId;
105
116
  const focused = state.focusedPaneId ? resolvePaneInstance(state.config.layout, state.focusedPaneId) : null;
106
117
  if (focused?.paneId === TICKER_RESEARCH_PANE_ID) return focused.instanceId;
107
- const sourcePaneId = resolveCollectionSourcePaneId(preferredPaneId);
118
+ const sourcePaneId = resolveTickerSourcePaneId(preferredPaneId);
108
119
  if (!sourcePaneId) return null;
109
120
  const ensured = ensureInspectorPane(sourcePaneId);
110
121
  if (!ensured) return null;
@@ -120,14 +131,14 @@ export function useAppTickerInspectorRuntime({
120
131
  dispatch,
121
132
  ensureInspectorPane,
122
133
  persistLayout,
123
- resolveCollectionSourcePaneId,
134
+ resolveTickerSourcePaneId,
124
135
  state.config.layout,
125
136
  state.focusedPaneId,
126
137
  ]);
127
138
 
128
139
  const buildPaneBinding = useCallback((paneType: string, preferredPaneId?: string | null): PaneBinding | null => {
129
140
  if (normalizePaneId(paneType) === TICKER_RESEARCH_PANE_ID) {
130
- const sourceInstanceId = resolveCollectionSourcePaneId(preferredPaneId);
141
+ const sourceInstanceId = resolveTickerSourcePaneId(preferredPaneId);
131
142
  return sourceInstanceId ? { kind: "follow", sourceInstanceId } : null;
132
143
  }
133
144
  if (isTickerPaneId(paneType)) {
@@ -135,12 +146,12 @@ export function useAppTickerInspectorRuntime({
135
146
  return sourceInstanceId ? { kind: "follow", sourceInstanceId } : null;
136
147
  }
137
148
  return { kind: "none" };
138
- }, [resolveCollectionSourcePaneId, resolveTickerContextSourcePaneId]);
149
+ }, [resolveTickerContextSourcePaneId, resolveTickerSourcePaneId]);
139
150
 
140
151
  const showTickerResearchPane = useCallback(() => {
141
- const sourcePaneId = resolveCollectionSourcePaneId();
152
+ const sourcePaneId = resolveTickerSourcePaneId();
142
153
  if (!sourcePaneId) {
143
- notify("Open a collection pane first to inspect a ticker.");
154
+ notify("Open a ticker source pane first to inspect a ticker.");
144
155
  return;
145
156
  }
146
157
  const ensured = ensureInspectorPane(sourcePaneId);
@@ -154,7 +165,7 @@ export function useAppTickerInspectorRuntime({
154
165
  ensureInspectorPane,
155
166
  notify,
156
167
  persistLayout,
157
- resolveCollectionSourcePaneId,
168
+ resolveTickerSourcePaneId,
158
169
  state.config.layout,
159
170
  ]);
160
171
 
@@ -101,6 +101,22 @@ describe("desktop deeplinks", () => {
101
101
  });
102
102
  });
103
103
 
104
+ test("routes only opaque 32-character social share ids", () => {
105
+ const id = "0123456789abcdef0123456789abcdef";
106
+ expect(resolveDesktopDeepLinkAction(`gloomberb://share/${id}`)).toEqual({
107
+ type: "open-share",
108
+ id,
109
+ message: "Opened shared pane.",
110
+ });
111
+ expect(resolveDesktopDeepLinkAction(`gloomberb://layout/${id}`)).toEqual({
112
+ type: "open-layout",
113
+ id,
114
+ message: "Added shared layout.",
115
+ });
116
+ expect(resolveDesktopDeepLinkAction("gloomberb://share/not-valid").type).toBe("unsupported");
117
+ expect(resolveDesktopDeepLinkAction("gloomberb://layout/not-valid").type).toBe("unsupported");
118
+ });
119
+
104
120
  test("rejects command-bar, non-gloomberb, and malformed links", () => {
105
121
  expect(resolveDesktopDeepLinkAction("gloomberb://command?query=profile").type).toBe("unsupported");
106
122
  expect(resolveDesktopDeepLinkAction("gloomberb://search/NVDA").type).toBe("unsupported");
@@ -1,4 +1,5 @@
1
1
  import { useEffect, type Dispatch } from "react";
2
+ import { apiClient } from "../../api-client";
2
3
  import { getDockedPaneIds } from "../../plugins/pane-manager";
3
4
  import type { PluginRegistry } from "../../plugins/registry";
4
5
  import type { AppAction, AppState } from "../../state/app/context";
@@ -11,6 +12,9 @@ import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
11
12
  import type { DesktopWindowBridge } from "../../types/desktop-window";
12
13
  import { requestAccountManagementTab } from "../../plugins/builtin/account-management/navigation";
13
14
  import { chatController } from "../../plugins/builtin/chat/controller";
15
+ import { getShare } from "../../shares/api";
16
+ import { openPaneShare } from "../../shares/pane";
17
+ import { materializeMarketplaceLayout } from "../../layout-marketplace/payload";
14
18
 
15
19
  type CloudDeepLinkRoute = {
16
20
  kind: "cloud-alerts" | "cloud-emails" | "cloud-roundup" | "cloud-success";
@@ -20,6 +24,7 @@ type CloudDeepLinkRoute = {
20
24
  type CollectionDeepLinkKind = "collection" | "portfolio" | "watchlist";
21
25
  type AlertDeepLinkCondition = "above" | "below" | "crosses";
22
26
  type NewsDeepLinkKind = "breaking" | "feed" | "ticker" | "top";
27
+ const SHARE_ID = /^[a-f0-9]{32}$/;
23
28
 
24
29
  export type DesktopDeepLinkAction =
25
30
  | { type: "open-account-management"; route: CloudDeepLinkRoute; message: string }
@@ -33,6 +38,8 @@ export type DesktopDeepLinkAction =
33
38
  | { type: "open-chat-channel"; channelId: string; messageId: string | null; message: string }
34
39
  | { type: "open-chat-dm"; participants: string; message: string }
35
40
  | { type: "open-news"; kind: NewsDeepLinkKind; symbol: string | null; message: string }
41
+ | { type: "open-share"; id: string; message: string }
42
+ | { type: "open-layout"; id: string; message: string }
36
43
  | { type: "unsupported"; message: string };
37
44
 
38
45
  interface ParsedGloomUrl {
@@ -241,6 +248,20 @@ function parseNewsDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
241
248
  return { type: "unsupported", message: "Unsupported Gloomberb news link." };
242
249
  }
243
250
 
251
+ function parseLayoutDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
252
+ const id = parsed.segments[0] ?? param(parsed.url, "id", "layout");
253
+ return id && SHARE_ID.test(id)
254
+ ? { type: "open-layout", id, message: "Added shared layout." }
255
+ : { type: "unsupported", message: "Layout links need a valid id." };
256
+ }
257
+
258
+ function parseShareDeepLink(parsed: ParsedGloomUrl): DesktopDeepLinkAction {
259
+ const id = parsed.segments[0] ?? param(parsed.url, "id", "share");
260
+ return id && SHARE_ID.test(id)
261
+ ? { type: "open-share", id, message: "Opened shared pane." }
262
+ : { type: "unsupported", message: "Share links need a valid id." };
263
+ }
264
+
244
265
  export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAction {
245
266
  const parsed = parseGloomUrl(rawUrl);
246
267
  if (!parsed) return { type: "unsupported", message: "Unsupported Gloomberb link." };
@@ -262,6 +283,10 @@ export function resolveDesktopDeepLinkAction(rawUrl: string): DesktopDeepLinkAct
262
283
  return parseChatDeepLink(parsed);
263
284
  case "news":
264
285
  return parseNewsDeepLink(parsed);
286
+ case "share":
287
+ return parseShareDeepLink(parsed);
288
+ case "layout":
289
+ return parseLayoutDeepLink(parsed);
265
290
  default:
266
291
  return { type: "unsupported", message: "Unsupported Gloomberb link." };
267
292
  }
@@ -462,6 +487,33 @@ function handleOpenNews(
462
487
  notifySuccess(pluginRegistry, action.message);
463
488
  }
464
489
 
490
+ async function handleOpenLayout(
491
+ action: Extract<DesktopDeepLinkAction, { type: "open-layout" }>,
492
+ options: DesktopDeepLinkHandlerOptions,
493
+ ): Promise<void> {
494
+ const entry = await apiClient.getMarketplaceLayout(action.id);
495
+ if (!entry) throw new Error("This shared layout is unavailable.");
496
+ const installed = materializeMarketplaceLayout(entry);
497
+ options.dispatch({
498
+ type: "INSTALL_LAYOUT_COPY",
499
+ name: entry.name,
500
+ layout: installed.layout,
501
+ paneState: installed.paneState,
502
+ });
503
+ notifySuccess(options.pluginRegistry, `Layout "${entry.name}" added.`);
504
+ }
505
+
506
+ async function handleOpenShare(
507
+ action: Extract<DesktopDeepLinkAction, { type: "open-share" }>,
508
+ pluginRegistry: PluginRegistry,
509
+ ): Promise<void> {
510
+ const share = await getShare(action.id, fetch, { trackView: false });
511
+ if (!share) throw new Error("This share is unavailable or has expired.");
512
+ if (share.kind !== "pane") throw new Error("This link contains a snapshot, not a live pane.");
513
+ await openPaneShare(pluginRegistry, share.data);
514
+ notifySuccess(pluginRegistry, action.message);
515
+ }
516
+
465
517
  export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHandlerOptions): void {
466
518
  const action = resolveDesktopDeepLinkAction(rawUrl);
467
519
  if (action.type === "unsupported") {
@@ -499,6 +551,16 @@ export function handleDesktopDeepLink(rawUrl: string, options: DesktopDeepLinkHa
499
551
  case "open-news":
500
552
  handleOpenNews(action, options.pluginRegistry);
501
553
  return;
554
+ case "open-share":
555
+ void handleOpenShare(action, options.pluginRegistry).catch((error) => {
556
+ notifyError(options.pluginRegistry, error instanceof Error ? error.message : "Could not open shared pane.");
557
+ });
558
+ return;
559
+ case "open-layout":
560
+ void handleOpenLayout(action, options).catch((error) => {
561
+ notifyError(options.pluginRegistry, error instanceof Error ? error.message : "Could not open shared layout.");
562
+ });
563
+ return;
502
564
  }
503
565
  }
504
566
 
@@ -506,19 +568,21 @@ export function useDesktopDeepLinkRuntime({
506
568
  desktopDeepLinkBridge,
507
569
  desktopWindowKind,
508
570
  dispatch,
571
+ initialized,
509
572
  pluginRegistry,
510
573
  stateRef,
511
574
  }: {
512
575
  desktopDeepLinkBridge?: DesktopDeepLinkBridge;
513
576
  desktopWindowKind?: DesktopWindowBridge["kind"];
514
577
  dispatch: Dispatch<AppAction>;
578
+ initialized: boolean;
515
579
  pluginRegistry: PluginRegistry;
516
580
  stateRef: { current: AppState };
517
581
  }) {
518
582
  useEffect(() => {
519
- if (desktopWindowKind !== "main" || !desktopDeepLinkBridge) return;
583
+ if (!initialized || desktopWindowKind === "detached" || !desktopDeepLinkBridge) return;
520
584
  return desktopDeepLinkBridge.subscribe((deeplink) => {
521
585
  handleDesktopDeepLink(deeplink.url, { dispatch, pluginRegistry, stateRef });
522
586
  });
523
- }, [desktopDeepLinkBridge, desktopWindowKind, dispatch, pluginRegistry, stateRef]);
587
+ }, [desktopDeepLinkBridge, desktopWindowKind, dispatch, initialized, pluginRegistry, stateRef]);
524
588
  }
@@ -46,6 +46,9 @@ export function useDesktopApplicationMenuRuntime({
46
46
  case "toggle-status-bar":
47
47
  dispatch({ type: "TOGGLE_STATUS_BAR" });
48
48
  break;
49
+ case "open-layout-gallery":
50
+ pluginRegistry.showPane("layout-marketplace");
51
+ break;
49
52
  case "layout-undo":
50
53
  dispatch({ type: "UNDO_LAYOUT" });
51
54
  break;
@@ -23,7 +23,7 @@ export function bindPluginRegistryRuntimeAccess({
23
23
  importBrokerPositions,
24
24
  marketData,
25
25
  pluginRegistry,
26
- state,
26
+ stateRef,
27
27
  tickerRepository,
28
28
  }: {
29
29
  dataProvider: DataProvider;
@@ -31,31 +31,32 @@ export function bindPluginRegistryRuntimeAccess({
31
31
  importBrokerPositions: (instanceId: string) => Promise<unknown>;
32
32
  marketData: MarketDataCoordinator;
33
33
  pluginRegistry: PluginRegistry;
34
- state: AppState;
34
+ stateRef: { current: AppState };
35
35
  tickerRepository: AppTickerRepositoryPort;
36
36
  }) {
37
- pluginRegistry.getTickerFn = (symbol) => state.tickers.get(symbol) ?? null;
37
+ pluginRegistry.getTickerFn = (symbol) => stateRef.current.tickers.get(symbol) ?? null;
38
38
  pluginRegistry.getDataFn = (symbol) => {
39
- const ticker = state.tickers.get(symbol) ?? null;
39
+ const ticker = stateRef.current.tickers.get(symbol) ?? null;
40
40
  const instrument = instrumentFromTicker(ticker, symbol);
41
41
  return instrument ? marketData.getTickerFinancialsSync(instrument) : null;
42
42
  };
43
- pluginRegistry.getConfigFn = () => state.config;
44
- pluginRegistry.getPaneRuntimeStateFn = (paneId) => state.paneState[paneId] ?? null;
43
+ pluginRegistry.getConfigFn = () => stateRef.current.config;
44
+ pluginRegistry.getPaneRuntimeStateFn = (paneId) => stateRef.current.paneState[paneId] ?? null;
45
45
  pluginRegistry.updatePaneRuntimeStateFn = (paneId, patch) => {
46
46
  dispatch({ type: "UPDATE_PANE_STATE", paneId, patch });
47
47
  };
48
48
  pluginRegistry.getPluginConfigValueFn = (pluginId, key) => (
49
- (state.config.pluginConfig[pluginId]?.[key] as any) ?? null
49
+ (stateRef.current.config.pluginConfig[pluginId]?.[key] as any) ?? null
50
50
  );
51
51
 
52
52
  const setPluginConfigValues = async (pluginId: string, values: Record<string, unknown>) => {
53
+ const currentConfig = stateRef.current.config;
53
54
  const nextConfig = {
54
- ...state.config,
55
+ ...currentConfig,
55
56
  pluginConfig: {
56
- ...state.config.pluginConfig,
57
+ ...currentConfig.pluginConfig,
57
58
  [pluginId]: {
58
- ...(state.config.pluginConfig[pluginId] ?? {}),
59
+ ...(currentConfig.pluginConfig[pluginId] ?? {}),
59
60
  ...values,
60
61
  },
61
62
  },
@@ -70,13 +71,14 @@ export function bindPluginRegistryRuntimeAccess({
70
71
  };
71
72
  pluginRegistry.setPluginConfigValuesFn = setPluginConfigValues;
72
73
  pluginRegistry.deletePluginConfigValueFn = async (pluginId, key) => {
73
- const currentPluginConfig = state.config.pluginConfig[pluginId];
74
+ const currentConfig = stateRef.current.config;
75
+ const currentPluginConfig = currentConfig.pluginConfig[pluginId];
74
76
  if (!currentPluginConfig || !(key in currentPluginConfig)) return;
75
77
 
76
78
  const nextPluginConfig = { ...currentPluginConfig };
77
79
  delete nextPluginConfig[key];
78
80
 
79
- const nextAllPluginConfig = { ...state.config.pluginConfig };
81
+ const nextAllPluginConfig = { ...currentConfig.pluginConfig };
80
82
  if (Object.keys(nextPluginConfig).length === 0) {
81
83
  delete nextAllPluginConfig[pluginId];
82
84
  } else {
@@ -84,7 +86,7 @@ export function bindPluginRegistryRuntimeAccess({
84
86
  }
85
87
 
86
88
  const nextConfig = {
87
- ...state.config,
89
+ ...currentConfig,
88
90
  pluginConfig: nextAllPluginConfig,
89
91
  };
90
92
  dispatch({ type: "SET_CONFIG", config: nextConfig });
@@ -96,14 +98,14 @@ export function bindPluginRegistryRuntimeAccess({
96
98
  setConfigAccessor?: (accessor: () => AppConfig) => void;
97
99
  };
98
100
  if (typeof configurableProvider.setConfigAccessor === "function") {
99
- configurableProvider.setConfigAccessor(() => state.config);
101
+ configurableProvider.setConfigAccessor(() => stateRef.current.config);
100
102
  }
101
103
 
102
104
  pluginRegistry.createBrokerInstanceFn = async (brokerType, label, values) => {
103
105
  const instanceId = createBrokerInstanceId(
104
106
  brokerType,
105
107
  label,
106
- state.config.brokerInstances.map((instance) => instance.id),
108
+ stateRef.current.config.brokerInstances.map((instance) => instance.id),
107
109
  );
108
110
  const instance: BrokerInstanceConfig = {
109
111
  id: instanceId,
@@ -114,8 +116,8 @@ export function bindPluginRegistryRuntimeAccess({
114
116
  enabled: true,
115
117
  };
116
118
  const nextConfig = {
117
- ...state.config,
118
- brokerInstances: [...state.config.brokerInstances, instance],
119
+ ...stateRef.current.config,
120
+ brokerInstances: [...stateRef.current.config.brokerInstances, instance],
119
121
  };
120
122
  dispatch({ type: "SET_CONFIG", config: nextConfig });
121
123
  await saveConfigImmediately(nextConfig);
@@ -124,7 +126,7 @@ export function bindPluginRegistryRuntimeAccess({
124
126
  };
125
127
 
126
128
  pluginRegistry.connectBrokerInstanceFn = async (instanceId) => {
127
- const instance = getBrokerInstance(state.config.brokerInstances, instanceId);
129
+ const instance = getBrokerInstance(stateRef.current.config.brokerInstances, instanceId);
128
130
  if (!instance) throw new Error("Broker profile not found.");
129
131
  if (instance.enabled === false) throw new Error(`Broker profile "${instance.label}" is disabled.`);
130
132
 
@@ -142,8 +144,8 @@ export function bindPluginRegistryRuntimeAccess({
142
144
  };
143
145
 
144
146
  pluginRegistry.updateBrokerInstanceFn = async (instanceId, values, options = {}) => {
145
- const currentInstance = state.config.brokerInstances.find((instance) => instance.id === instanceId);
146
- const nextInstances = state.config.brokerInstances.map((instance) =>
147
+ const currentInstance = stateRef.current.config.brokerInstances.find((instance) => instance.id === instanceId);
148
+ const nextInstances = stateRef.current.config.brokerInstances.map((instance) =>
147
149
  instance.id === instanceId
148
150
  ? (() => {
149
151
  const nextValues = options.replaceConfig ? values : { ...instance.config, ...values };
@@ -167,7 +169,7 @@ export function bindPluginRegistryRuntimeAccess({
167
169
  clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, currentInstance);
168
170
  }
169
171
  const nextConfig = {
170
- ...state.config,
172
+ ...stateRef.current.config,
171
173
  brokerInstances: nextInstances,
172
174
  };
173
175
  dispatch({ type: "SET_CONFIG", config: nextConfig });
@@ -180,7 +182,7 @@ export function bindPluginRegistryRuntimeAccess({
180
182
  };
181
183
 
182
184
  pluginRegistry.removeBrokerInstanceFn = async (instanceId) => {
183
- const instance = getBrokerInstance(state.config.brokerInstances, instanceId);
185
+ const instance = getBrokerInstance(stateRef.current.config.brokerInstances, instanceId);
184
186
  if (!instance) return;
185
187
 
186
188
  clearPersistedBrokerAccounts(pluginRegistry.persistence.resources, instance);
@@ -189,15 +191,15 @@ export function bindPluginRegistryRuntimeAccess({
189
191
  await broker?.disconnect?.(instance).catch(() => {});
190
192
 
191
193
  const removedPortfolioIds = new Set(
192
- state.config.portfolios
194
+ stateRef.current.config.portfolios
193
195
  .filter((portfolio) => portfolio.brokerInstanceId === instanceId)
194
196
  .map((portfolio) => portfolio.id),
195
197
  );
196
198
 
197
- const nextPortfolios = state.config.portfolios.filter((portfolio) => !removedPortfolioIds.has(portfolio.id));
198
- const nextTickers = new Map(state.tickers);
199
+ const nextPortfolios = stateRef.current.config.portfolios.filter((portfolio) => !removedPortfolioIds.has(portfolio.id));
200
+ const nextTickers = new Map(stateRef.current.tickers);
199
201
 
200
- for (const ticker of state.tickers.values()) {
202
+ for (const ticker of stateRef.current.tickers.values()) {
201
203
  const nextPositions = ticker.metadata.positions.filter((position) => position.brokerInstanceId !== instanceId);
202
204
  const nextPortfolioRefs = ticker.metadata.portfolios.filter((portfolioId) => !removedPortfolioIds.has(portfolioId));
203
205
  const nextBrokerContracts = (ticker.metadata.broker_contracts ?? []).filter((contract) => contract.brokerInstanceId !== instanceId);
@@ -233,8 +235,8 @@ export function bindPluginRegistryRuntimeAccess({
233
235
  }
234
236
 
235
237
  const nextConfig = {
236
- ...state.config,
237
- brokerInstances: state.config.brokerInstances.filter((entry) => entry.id !== instanceId),
238
+ ...stateRef.current.config,
239
+ brokerInstances: stateRef.current.config.brokerInstances.filter((entry) => entry.id !== instanceId),
238
240
  portfolios: nextPortfolios,
239
241
  };
240
242