gloomberb 0.11.1 → 0.11.3

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 (282) hide show
  1. package/README.md +11 -9
  2. package/README.zh-CN.md +6 -6
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +12 -0
  5. package/src/api-client/index.test.ts +47 -0
  6. package/src/api-client/index.ts +66 -0
  7. package/src/api-client/paths.ts +16 -0
  8. package/src/api-client/types.ts +59 -0
  9. package/src/app/pane-runtime/index.tsx +4 -1
  10. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  11. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  12. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  13. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  14. package/src/app/runtime/desktop-deeplink.ts +66 -2
  15. package/src/app/runtime/desktop-menu.ts +3 -0
  16. package/src/app/runtime/plugin-bindings.ts +30 -28
  17. package/src/app.tsx +9 -2
  18. package/src/capabilities/chart-series.ts +3 -2
  19. package/src/cli/commands/automation.test.ts +9 -0
  20. package/src/cli/commands/automation.ts +22 -73
  21. package/src/cli/commands/market.ts +2 -2
  22. package/src/cli/index.ts +0 -2
  23. package/src/cli/pane-functions/capabilities.ts +3 -3
  24. package/src/cli/registry.ts +0 -22
  25. package/src/cli/result.ts +7 -10
  26. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  27. package/src/components/chart/composite/composite-chart.tsx +12 -9
  28. package/src/components/chart/composite/scene.ts +0 -21
  29. package/src/components/chart/composite/tools.ts +49 -0
  30. package/src/components/command-bar/commands/direct/index.ts +4 -0
  31. package/src/components/command-bar/commands/registry.ts +8 -1
  32. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  33. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  34. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  35. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  36. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  37. package/src/components/command-bar/surface/index.test.tsx +6 -6
  38. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  39. package/src/components/command-bar/view-model.test.ts +2 -1
  40. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  41. package/src/components/command-bar/workflow/ops.ts +1 -1
  42. package/src/components/data-table/export.test.ts +21 -0
  43. package/src/components/data-table/export.ts +21 -0
  44. package/src/components/layout/detached-pane-shell.tsx +74 -10
  45. package/src/components/layout/pane/title.ts +31 -3
  46. package/src/components/layout/shell/active-drag.ts +4 -11
  47. package/src/components/layout/shell/drag/index.ts +4 -20
  48. package/src/components/layout/shell/drag/runtime.ts +3 -4
  49. package/src/components/layout/shell/index.test.tsx +17 -3
  50. package/src/components/layout/shell/index.tsx +86 -30
  51. package/src/components/layout/shell/menu.test.ts +37 -1
  52. package/src/components/layout/shell/menu.ts +14 -0
  53. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  54. package/src/components/layout/shell/pane/actions.ts +0 -5
  55. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  56. package/src/components/layout/shell/shortcuts.ts +6 -3
  57. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  58. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  59. package/src/components/layout/status-bar.test.tsx +62 -61
  60. package/src/components/layout/status-bar.tsx +53 -75
  61. package/src/components/ui/data-table/index.tsx +19 -2
  62. package/src/core/state/app/layout-reducer.ts +36 -0
  63. package/src/core/state/app/layout.ts +10 -5
  64. package/src/core/state/app/state.ts +0 -13
  65. package/src/core/state/app/types.ts +6 -4
  66. package/src/data/config/chart-settings.ts +5 -5
  67. package/src/data/config/layout.ts +2 -0
  68. package/src/data/config/store/index.test.ts +27 -6
  69. package/src/data/config/store/normalize.ts +3 -5
  70. package/src/data/plugin-state-store.ts +11 -0
  71. package/src/data/sqlite/cache.test.ts +10 -0
  72. package/src/i18n/es.ts +3 -4
  73. package/src/i18n/ja.ts +3 -4
  74. package/src/i18n/ko.ts +3 -4
  75. package/src/i18n/zh-cn.ts +3 -4
  76. package/src/i18n/zh-tw.ts +3 -4
  77. package/src/layout-marketplace/api.test.ts +54 -0
  78. package/src/layout-marketplace/api.ts +65 -0
  79. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  80. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  81. package/src/layout-marketplace/gallery-terminal.tsx +406 -0
  82. package/src/layout-marketplace/gallery.test.tsx +148 -0
  83. package/src/layout-marketplace/gallery.tsx +323 -0
  84. package/src/layout-marketplace/geometry.test.ts +47 -0
  85. package/src/layout-marketplace/geometry.ts +80 -0
  86. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  87. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  88. package/src/layout-marketplace/model.test.ts +118 -0
  89. package/src/layout-marketplace/model.ts +171 -0
  90. package/src/layout-marketplace/name-dialog.tsx +60 -0
  91. package/src/layout-marketplace/pane-imagery.ts +85 -0
  92. package/src/layout-marketplace/pane.tsx +20 -0
  93. package/src/layout-marketplace/payload.test.ts +246 -0
  94. package/src/layout-marketplace/payload.ts +624 -0
  95. package/src/layout-marketplace/use-marketplace.ts +63 -0
  96. package/src/market-data/hooks.tsx +0 -41
  97. package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
  98. package/src/plugins/broker-sync/normalize.ts +9 -2
  99. package/src/plugins/builtin/ai/index.tsx +18 -2
  100. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  101. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  102. package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
  103. package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
  104. package/src/plugins/builtin/alerts/index.tsx +4 -0
  105. package/src/plugins/builtin/alerts/pane.tsx +2 -8
  106. package/src/plugins/builtin/analytics/index.tsx +3 -0
  107. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  108. package/src/plugins/builtin/cds/client.test.ts +83 -0
  109. package/src/plugins/builtin/cds/client.ts +67 -0
  110. package/src/plugins/builtin/cds/index.tsx +41 -0
  111. package/src/plugins/builtin/cds/model.test.ts +232 -0
  112. package/src/plugins/builtin/cds/model.ts +401 -0
  113. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  114. package/src/plugins/builtin/cds/pane.tsx +358 -0
  115. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  116. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  117. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  118. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  119. package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
  120. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  121. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  122. package/src/plugins/builtin/chat/controller/index.ts +8 -1
  123. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  124. package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
  125. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  126. package/src/plugins/builtin/chat/controller.test.ts +64 -14
  127. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  128. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  130. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  132. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  133. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  134. package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
  135. package/src/plugins/builtin/cloud/auth-model.ts +4 -1
  136. package/src/plugins/builtin/cloud/plugin.tsx +11 -1
  137. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  138. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  139. package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
  140. package/src/plugins/builtin/composite-plugins.ts +5 -1
  141. package/src/plugins/builtin/connections/index.ts +3 -0
  142. package/src/plugins/builtin/earnings/index.tsx +1 -0
  143. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  144. package/src/plugins/builtin/econ/index.tsx +1 -0
  145. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  146. package/src/plugins/builtin/futures/index.tsx +1 -0
  147. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  148. package/src/plugins/builtin/help/index.tsx +2 -2
  149. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  150. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  151. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  152. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  153. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  154. package/src/plugins/builtin/news/index.tsx +1 -0
  155. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  156. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  157. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  158. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  159. package/src/plugins/builtin/options/analytics.ts +110 -0
  160. package/src/plugins/builtin/options/index.tsx +18 -2
  161. package/src/plugins/builtin/options/table.ts +121 -51
  162. package/src/plugins/builtin/options/types.ts +22 -15
  163. package/src/plugins/builtin/options/view.test.tsx +49 -3
  164. package/src/plugins/builtin/options/view.tsx +92 -19
  165. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  166. package/src/plugins/builtin/plugin-module.ts +0 -7
  167. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
  168. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  169. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  170. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  171. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  172. package/src/plugins/builtin/portfolio-list/index.tsx +9 -0
  173. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  174. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  175. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  176. package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
  177. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  178. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
  179. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
  180. package/src/plugins/builtin/research/index.tsx +4 -0
  181. package/src/plugins/builtin/scanner/index.ts +2 -0
  182. package/src/plugins/builtin/sectors/index.tsx +1 -0
  183. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  184. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  185. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  186. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  187. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  188. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  189. package/src/plugins/builtin/ticker-detail/index.tsx +12 -0
  190. package/src/plugins/builtin/treasury-auctions/index.tsx +3 -0
  191. package/src/plugins/builtin/world-indices/index.tsx +3 -0
  192. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  193. package/src/plugins/builtin/world-venue-map/map.tsx +337 -0
  194. package/src/plugins/builtin/world-venue-map/model.test.ts +53 -0
  195. package/src/plugins/builtin/world-venue-map/model.ts +147 -0
  196. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  197. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  198. package/src/plugins/catalog-backend.ts +6 -44
  199. package/src/plugins/catalog-browser.test.ts +10 -0
  200. package/src/plugins/catalog-browser.ts +5 -1
  201. package/src/plugins/gridlock-notification.ts +2 -1
  202. package/src/plugins/pane-manager/gridlock.ts +87 -3
  203. package/src/plugins/pane-manager.test.ts +30 -0
  204. package/src/plugins/pane-manager.ts +6 -1
  205. package/src/plugins/prediction-markets/chart.tsx +2 -2
  206. package/src/plugins/prediction-markets/controller/index.ts +5 -1
  207. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  208. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  209. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  210. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  211. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  212. package/src/plugins/prediction-markets/index.tsx +8 -4
  213. package/src/plugins/prediction-markets/navigation.ts +0 -1
  214. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  215. package/src/plugins/prediction-markets/pane.tsx +4 -0
  216. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  217. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  218. package/src/plugins/registry/index.ts +2 -0
  219. package/src/plugins/registry/pane-settings.test.ts +53 -0
  220. package/src/plugins/registry/pane-settings.ts +36 -3
  221. package/src/plugins/ticker-navigation.test.ts +24 -0
  222. package/src/plugins/ticker-navigation.ts +40 -0
  223. package/src/remote/schema.ts +1 -1
  224. package/src/renderers/browser/cloud-transport.test.ts +36 -0
  225. package/src/renderers/browser/cloud-transport.ts +19 -0
  226. package/src/renderers/browser/config-host.ts +0 -4
  227. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  228. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  229. package/src/renderers/browser/main.tsx +3 -0
  230. package/src/renderers/browser/ui-host.tsx +11 -0
  231. package/src/renderers/cloudflare/worker.test.ts +8 -5
  232. package/src/renderers/cloudflare/worker.ts +6 -5
  233. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  234. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  235. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  236. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  237. package/src/renderers/electrobun/bun/index.ts +1 -0
  238. package/src/renderers/electrobun/shared/protocol.ts +4 -0
  239. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  240. package/src/renderers/electrobun/view/host/native.ts +0 -5
  241. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  242. package/src/renderers/electrobun/view/styles.css +107 -7
  243. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  244. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  245. package/src/renderers/electrobun/view/ui-host.tsx +7 -1
  246. package/src/renderers/opentui/host.tsx +4 -0
  247. package/src/renderers/share/layout-view.test.tsx +58 -0
  248. package/src/renderers/share/layout-view.tsx +133 -0
  249. package/src/renderers/share/main.tsx +54 -6
  250. package/src/renderers/share/styles.css +35 -0
  251. package/src/renderers/share/view.test.tsx +78 -0
  252. package/src/renderers/share/view.tsx +71 -0
  253. package/src/shares/api.test.ts +52 -3
  254. package/src/shares/api.ts +39 -15
  255. package/src/shares/location.test.ts +19 -0
  256. package/src/shares/location.ts +11 -0
  257. package/src/shares/pane.test.ts +110 -0
  258. package/src/shares/pane.ts +54 -0
  259. package/src/shares/payload.ts +74 -1
  260. package/src/state/app/bootstrap.ts +6 -7
  261. package/src/state/app/context/index.test.ts +41 -1
  262. package/src/state/app/context/index.test.tsx +34 -16
  263. package/src/state/pane-table-export-registry.ts +31 -0
  264. package/src/state/selectors-ui.ts +0 -8
  265. package/src/sync/controller.test.ts +57 -1
  266. package/src/sync/controller.ts +22 -11
  267. package/src/sync/react.ts +13 -0
  268. package/src/theme/font-scale.ts +0 -11
  269. package/src/time-series/range.ts +12 -0
  270. package/src/time-series/resolution.ts +3 -15
  271. package/src/time-series/spec.ts +5 -5
  272. package/src/types/config.ts +35 -5
  273. package/src/types/desktop-menu.ts +1 -0
  274. package/src/types/plugin.ts +38 -20
  275. package/src/ui/context-menu.tsx +1 -1
  276. package/src/ui/host.tsx +7 -0
  277. package/src/ui/toast.tsx +2 -0
  278. package/src/utils/back-navigation.ts +0 -4
  279. package/src/utils/csv.test.ts +13 -0
  280. package/src/utils/csv.ts +37 -0
  281. package/src/utils/save-text-file.test.ts +24 -0
  282. package/src/utils/save-text-file.ts +36 -0
package/README.md CHANGED
@@ -46,7 +46,7 @@ bun run web:build
46
46
  bunx wrangler dev
47
47
  ```
48
48
 
49
- Validate the public artifacts with `bun run web:audit` and `bun run cloudflare:dry-run`. `wrangler.jsonc` is intentionally generic and contains no account, route, domain, namespace, or secret. Production routing and the private Gloom Cloud API deployment are configured separately.
49
+ Validate the public artifacts with `bun run web:audit` and `bun run cloudflare:dry-run`. `wrangler.jsonc` remains generic for local checks. After verification passes on `main`, GitHub Actions deploys `term.gloom.sh` with `wrangler.production.jsonc`. The private Gloom Cloud API is deployed separately.
50
50
 
51
51
  ## Install
52
52
 
@@ -166,10 +166,9 @@ Human-readable output is the default. Automation can opt into structured output
166
166
  | `gloomberb portfolio [action]` | Manage manual portfolios |
167
167
  | `gloomberb watchlist [action]` | Manage watchlists |
168
168
  | `gloomberb notes|alerts [action]` | Manage local notes and alerts |
169
- | `gloomberb broker|ibkr [action]` | Inspect broker integrations; trading actions require explicit account/profile and `--yes` |
170
- | `gloomberb ai providers|ask|screen` | Use configured AI providers and screeners |
169
+ | `gloomberb broker|ibkr [action]` | Inspect broker profiles |
170
+ | `gloomberb ai providers|ask` | Use configured AI providers |
171
171
  | `gloomberb rss fetch <url>` | Fetch an RSS feed |
172
- | `gloomberb buildout|congress|substack|x-feed|tweets` | Access cloud and social data sources when an existing session is available |
173
172
  | `gloomberb provider status` | Inspect enabled data providers |
174
173
  | `gloomberb config|cache|plugin|layout|pane|debug|doctor|version|changelog` | Inspect and manage local app state |
175
174
  | `gloomberb fn [...]` | Run a pane-backed report command |
@@ -180,8 +179,6 @@ Human-readable output is the default. Automation can opt into structured output
180
179
  | `gloomberb remove <name>` | Remove an installed plugin |
181
180
  | `gloomberb update [name]` | Update plugins |
182
181
 
183
- Commands that need a signed-in cloud session may return `auth_required`; sign-in, account management, and chat workflows are still handled in the app UI for now.
184
-
185
182
  ## Plugins
186
183
 
187
184
  Everything from the portfolio list to broker integrations is a plugin. Plugins can add panes, tabs, columns, command bar commands, CLI commands, status bar widgets, and data providers.
@@ -206,7 +203,7 @@ See [PLUGINS.md](PLUGINS.md) for the plugin API and the shared UI surface availa
206
203
  | `Ctrl+Shift+M` | Move focused window (`WIN resize` starts resize mode) |
207
204
  | `Ctrl+Shift+D` | Dock or float focused pane |
208
205
  | `Ctrl+Shift+L` | Layout actions |
209
- | `Ctrl+Shift+G` | Gridlock all windows |
206
+ | `Ctrl+Shift+G` | Tidy windows |
210
207
  | `Tab` | Switch panes |
211
208
  | `j` / `k` | Navigate lists |
212
209
  | `h` / `l` | Switch tabs |
@@ -283,12 +280,14 @@ The toolbar controls preset or exact date ranges, intervals from one minute thro
283
280
  | `TBO` | TheBuildout infrastructure intelligence |
284
281
  | `CG` | Congress trading disclosures |
285
282
  | `WEI` | Global equity indices |
283
+ | `MAP` | Live world venue map with local market status and clocks |
286
284
  | `FUT` | Front-month futures across index, rates, energy, metals, grains, and FX |
287
285
  | `ECON` | Economic events and releases |
288
286
  | `GC` | Yield curve |
289
287
  | `AUCT` | Treasury auction results: high rate, bid-to-cover, indirect share, and size |
290
288
  | `VIX` | VIX 30-day/3-month implied-volatility curve |
291
289
  | `CRD` | Credit spreads |
290
+ | `CDS [ticker]` | Single-name corporate CDS activity: most-active issuers, or one issuer's trades |
292
291
  | `ERN` | Earnings calendar |
293
292
  | `IPO` | Upcoming and recent IPOs |
294
293
  | `HALT` | US trading halts with reason and resumption times |
@@ -318,9 +317,10 @@ The toolbar controls preset or exact date ranges, intervals from one minute thro
318
317
  | `AW` / `AP <ticker>` | Add a ticker to the active watchlist or portfolio |
319
318
  | `RW` / `RP <ticker>` | Remove a ticker from the active watchlist or portfolio |
320
319
  | `PS` | Open focused pane settings |
321
- | `LAY <action>` | Open layout actions |
320
+ | `LAY` | Open the layout browser to switch, publish, or add layouts |
321
+ | `LMA <query>` | Layout and pane arrangement actions |
322
322
  | `WIN move\|resize` | Move or resize the focused window |
323
- | `GL` | Gridlock all visible panes |
323
+ | `GL` | Tidy all windows |
324
324
  | `SB` | Toggle the status bar |
325
325
  | `VF` | Toggle quote value flashing |
326
326
  | `TH <theme>` | Change color theme |
@@ -333,6 +333,8 @@ The toolbar controls preset or exact date ranges, intervals from one minute thro
333
333
  | `LANG <locale>` | Change interface language (`auto`, `en`, `es`, `zh-CN`, `zh-TW`, `ja`, or `ko`) |
334
334
  | `PL <plugin>` | Manage plugins |
335
335
 
336
+ Published layouts preserve portable pane setup and state, including searches, chart viewport, and drawings. Credentials, accounts, portfolios, and pane fields marked private stay local. Publishing copies a durable `term.gloom.sh/l/...` link for social sharing.
337
+
336
338
  ## Gloom Cloud sign-in
337
339
 
338
340
  Sign in with email and password, or pick `Log In with QR Code` from the command bar and scan the code with the Gloomberb mobile companion app to sign the terminal in without typing. The onboarding wizard offers the same QR option as the recommended path, with email and password as the alternative.
package/README.zh-CN.md CHANGED
@@ -136,10 +136,9 @@ gloomberb
136
136
  | `gloomberb portfolio [action]` | 管理手动投资组合 |
137
137
  | `gloomberb watchlist [action]` | 管理自选列表 |
138
138
  | `gloomberb notes\|alerts [action]` | 管理本地笔记与提醒 |
139
- | `gloomberb broker\|ibkr [action]` | 查看券商集成状态;交易操作需要显式指定账户/配置并加 `--yes` |
140
- | `gloomberb ai providers\|ask\|screen` | 使用已配置的 AI 服务商与选股器 |
139
+ | `gloomberb broker\|ibkr [action]` | 查看券商配置 |
140
+ | `gloomberb ai providers\|ask` | 使用已配置的 AI 服务商 |
141
141
  | `gloomberb rss fetch <url>` | 抓取 RSS 订阅源 |
142
- | `gloomberb buildout\|congress\|substack\|x-feed\|tweets` | 在已有会话可用时访问云端与社交数据源 |
143
142
  | `gloomberb provider status` | 查看已启用的数据源 |
144
143
  | `gloomberb config\|cache\|plugin\|layout\|pane\|debug\|doctor\|version\|changelog` | 查看和管理本地应用状态 |
145
144
  | `gloomberb fn [...]` | 运行基于面板的报告命令 |
@@ -150,8 +149,6 @@ gloomberb
150
149
  | `gloomberb remove <name>` | 移除已安装插件 |
151
150
  | `gloomberb update [name]` | 更新插件 |
152
151
 
153
- 需要已登录云端会话的命令可能返回 `auth_required`;登录、账户管理与聊天相关操作目前仍需在应用界面中完成。
154
-
155
152
  ## 插件
156
153
 
157
154
  从投资组合列表到券商集成,一切皆为插件。插件可以添加面板、页签、列、命令栏命令、CLI 命令、状态栏组件与数据源。
@@ -260,7 +257,8 @@ gloomberb
260
257
  | `AW` / `AP <ticker>` | 将股票添加到活动自选列表或投资组合 |
261
258
  | `RW` / `RP <ticker>` | 从活动自选列表或投资组合中移除股票 |
262
259
  | `PS` | 打开聚焦面板的设置 |
263
- | `LAY <action>` | 打开布局操作 |
260
+ | `LAY` | 打开布局浏览器以切换、发布或添加布局 |
261
+ | `LMA <query>` | 布局与面板排列操作 |
264
262
  | `WIN move\|resize` | 移动或缩放聚焦窗口 |
265
263
  | `GL` | 所有可见面板网格对齐 |
266
264
  | `SB` | 切换状态栏 |
@@ -270,6 +268,8 @@ gloomberb
270
268
  | `LANG <locale>` | 切换界面语言(`auto`、`en`、`es`、`zh-CN`、`zh-TW`、`ja` 或 `ko`) |
271
269
  | `PL <plugin>` | 管理插件 |
272
270
 
271
+ 发布的布局会保留可移植的面板设置与状态,包括搜索条件、图表视窗和绘图;凭据、账户、投资组合以及标记为私密的字段始终保留在本地。发布后会自动复制一个长期有效的 `term.gloom.sh/l/...` 社交分享链接。
272
+
273
273
  ## 本地化界面
274
274
 
275
275
  Gloomberb 内置 English、简体中文、繁體中文、日本語和한국어界面支持;英文仍是默认回退语言。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/index.tsx",
@@ -2,6 +2,7 @@ import type { TickerFinancials } from "../types/financials";
2
2
  import type { InstrumentSearchResult } from "../types/instrument";
3
3
  import { normalizeTweetSearchResponse } from "./normalizers";
4
4
  import {
5
+ cloudCdsPath,
5
6
  cloudCongressHousePath,
6
7
  cloudExchangeRatePath,
7
8
  cloudSec13FPath,
@@ -17,6 +18,7 @@ import {
17
18
  cloudStatementsPath,
18
19
  cloudTickerTweetsPath,
19
20
  cloudTweetSearchPath,
21
+ type CloudCdsParams,
20
22
  type CloudCongressHouseParams,
21
23
  type CloudFredSeriesParams,
22
24
  type CloudHistoryParams,
@@ -29,6 +31,7 @@ import {
29
31
  import type {
30
32
  CloudAnalystResearchPayload,
31
33
  CloudShortInterestPayload,
34
+ CloudCdsResponse,
32
35
  CloudCompanyProfile,
33
36
  CloudCongressHousePayload,
34
37
  CloudCorporateActionsPayload,
@@ -52,6 +55,7 @@ import type {
52
55
  CloudPricePointPayload,
53
56
  CloudQuotePayload,
54
57
  CloudTweetSearchResponse,
58
+ CloudWorldVenueMapPayload,
55
59
  CloudYieldPointPayload,
56
60
  } from "./types";
57
61
 
@@ -91,6 +95,10 @@ export class CloudDataApi {
91
95
  return this.postMarketBatch("/market/quotes/batch", targets, mode);
92
96
  }
93
97
 
98
+ async getCloudWorldVenues(): Promise<CloudMarketResponse<CloudWorldVenueMapPayload>> {
99
+ return this.request<CloudMarketResponse<CloudWorldVenueMapPayload>>("/market/venues");
100
+ }
101
+
94
102
  async getCloudMarketScreener(
95
103
  category: CloudMarketScreenerCategory,
96
104
  count = 25,
@@ -209,6 +217,10 @@ export class CloudDataApi {
209
217
  return this.request<CloudYieldPointPayload[]>("/cloud/econ/yield-curve");
210
218
  }
211
219
 
220
+ async getCloudCds(params: CloudCdsParams = {}): Promise<CloudCdsResponse> {
221
+ return this.request<CloudCdsResponse>(cloudCdsPath(params));
222
+ }
223
+
212
224
  async getCloudCongressHouse(params: CloudCongressHouseParams = {}): Promise<CloudCongressHousePayload> {
213
225
  return this.request<CloudCongressHousePayload>(cloudCongressHousePath(params));
214
226
  }
@@ -1,6 +1,9 @@
1
1
  import { afterEach, describe, expect, jest, test } from "bun:test";
2
2
  import type { AuthUser } from "./index";
3
3
  import { apiClient, setCloudApiFetchTransport } from "./index";
4
+ import { publishableMarketplaceLayout } from "../layout-marketplace/payload";
5
+ import { createDefaultConfig } from "../types/config";
6
+ import type { PaneDef } from "../types/plugin";
4
7
 
5
8
  const originalFetch = globalThis.fetch;
6
9
  const originalWebSocket = globalThis.WebSocket;
@@ -108,6 +111,50 @@ afterEach(() => {
108
111
  jest.useRealTimers();
109
112
  });
110
113
 
114
+ describe("apiClient layout marketplace", () => {
115
+ test("lists and publishes validated layouts through authenticated transport", async () => {
116
+ const config = createDefaultConfig("/tmp/api-layout-marketplace-test");
117
+ const panes = new Map(config.layout.instances.map((instance) => [instance.paneId, {
118
+ id: instance.paneId,
119
+ name: instance.paneId,
120
+ component: () => null,
121
+ defaultPosition: "right" as const,
122
+ } satisfies PaneDef]));
123
+ const payload = publishableMarketplaceLayout(config.layout, {}, panes);
124
+ const entry = {
125
+ id: "0123456789abcdef0123456789abcdef",
126
+ name: "Research Desk",
127
+ ...payload,
128
+ author: { username: "analyst", displayName: "Analyst" },
129
+ publishedAt: "2026-08-26T00:00:00.000Z",
130
+ };
131
+ const calls: Array<{ url: string; method: string; body?: unknown }> = [];
132
+ apiClient.setSessionToken("marketplace-session");
133
+ setCloudApiFetchTransport(async (url, init) => {
134
+ calls.push({
135
+ url,
136
+ method: init?.method ?? "GET",
137
+ ...(typeof init?.body === "string" ? { body: JSON.parse(init.body) } : {}),
138
+ });
139
+ const path = new URL(url).pathname;
140
+ return createResponse(path === `/layouts/${entry.id}` || init?.method === "POST"
141
+ ? entry
142
+ : { items: [entry] });
143
+ });
144
+
145
+ await expect(apiClient.listMarketplaceLayouts()).resolves.toEqual([entry]);
146
+ await expect(apiClient.getMarketplaceLayout(entry.id)).resolves.toEqual(entry);
147
+ await expect(apiClient.publishMarketplaceLayout(entry.name, payload)).resolves.toEqual(entry);
148
+
149
+ expect(calls.map((call) => [new URL(call.url).pathname, call.method])).toEqual([
150
+ ["/layouts", "GET"],
151
+ [`/layouts/${entry.id}`, "GET"],
152
+ ["/layouts", "POST"],
153
+ ]);
154
+ expect(calls[2]?.body).toMatchObject({ name: "Research Desk", schemaVersion: 2, paneState: {} });
155
+ });
156
+ });
157
+
111
158
  describe("apiClient auth cookies", () => {
112
159
  test("accepts a browser-managed api.gloom.sh cookie without exposing its value", async () => {
113
160
  apiClient.setCookieSessionMode(true);
@@ -3,9 +3,11 @@ import type { InstrumentSearchResult } from "../types/instrument";
3
3
  import { CloudAuthApi } from "./auth";
4
4
  import { CloudChatApi } from "./chat";
5
5
  import { CloudDataApi } from "./data";
6
+ import { ApiRequestError } from "./errors";
6
7
  import { CloudApiRequestTransport } from "./request";
7
8
  import { CloudApiSocket } from "./socket";
8
9
  import type {
10
+ CloudCdsParams,
9
11
  CloudCongressHouseParams,
10
12
  CloudFredSeriesParams,
11
13
  CloudSecFilingParams,
@@ -44,6 +46,7 @@ import type {
44
46
  CloudEconEventPayload,
45
47
  CloudEquityDiagnosticMode,
46
48
  CloudEquityDiagnosticResult,
49
+ CloudCdsResponse,
47
50
  CloudFredSeriesPayload,
48
51
  CloudYieldPointPayload,
49
52
  CloudCongressHousePayload,
@@ -58,6 +61,7 @@ import type {
58
61
  CloudMarketBatchPayload,
59
62
  CloudMarketScreenerCategory,
60
63
  CloudMarketScreenerPayload,
64
+ CloudWorldVenueMapPayload,
61
65
  CloudVerificationResponse,
62
66
  DeviceAuthStartResponse,
63
67
  DeviceAuthTokenResponse,
@@ -69,6 +73,13 @@ import type {
69
73
  ScannerKind,
70
74
  } from "./types";
71
75
  import type { SyncSettings, SyncSnapshot } from "../sync/types";
76
+ import {
77
+ isMarketplaceLayoutId,
78
+ parseMarketplaceLayoutEntry,
79
+ parseMarketplaceLayoutList,
80
+ type LayoutMarketplaceEntry,
81
+ type LayoutMarketplacePayload,
82
+ } from "../layout-marketplace/payload";
72
83
 
73
84
  export type * from "./types";
74
85
  export { setCloudApiFetchTransport } from "./request";
@@ -282,6 +293,14 @@ class GloomApiClient {
282
293
  return this.request<{ url: string }>("/stripe/checkout", { method: "POST", body: JSON.stringify({}) });
283
294
  }
284
295
 
296
+ /** Stores a verified user's public terminal snapshot or pane handoff. */
297
+ async createTerminalShare(payload: unknown): Promise<{ id: string; expiresAt: string }> {
298
+ return this.request<{ id: string; expiresAt: string }>("/shares", {
299
+ method: "POST",
300
+ body: JSON.stringify(payload),
301
+ });
302
+ }
303
+
285
304
  /** Stripe billing portal for an account that already has a subscription. */
286
305
  async createBillingPortal(): Promise<{ url: string }> {
287
306
  return this.request<{ url: string }>("/stripe/portal", { method: "POST", body: JSON.stringify({}) });
@@ -321,6 +340,45 @@ class GloomApiClient {
321
340
  });
322
341
  }
323
342
 
343
+ async getMarketplaceLayout(
344
+ id: string,
345
+ options?: { signal?: AbortSignal },
346
+ ): Promise<LayoutMarketplaceEntry | null> {
347
+ if (!isMarketplaceLayoutId(id)) return null;
348
+ try {
349
+ return parseMarketplaceLayoutEntry(await this.request<unknown>(`/layouts/${encodeURIComponent(id)}`, {
350
+ method: "GET",
351
+ signal: options?.signal,
352
+ }));
353
+ } catch (error) {
354
+ if (error instanceof ApiRequestError && error.status === 404) return null;
355
+ throw error;
356
+ }
357
+ }
358
+
359
+ async listMarketplaceLayouts(options?: { signal?: AbortSignal }): Promise<LayoutMarketplaceEntry[]> {
360
+ const items = parseMarketplaceLayoutList(await this.request<unknown>("/layouts", {
361
+ method: "GET",
362
+ signal: options?.signal,
363
+ }));
364
+ if (!items) throw new Error("The layout marketplace returned invalid data.");
365
+ return items;
366
+ }
367
+
368
+ async publishMarketplaceLayout(
369
+ name: string,
370
+ payload: LayoutMarketplacePayload,
371
+ options?: { signal?: AbortSignal },
372
+ ): Promise<LayoutMarketplaceEntry> {
373
+ const item = parseMarketplaceLayoutEntry(await this.request<unknown>("/layouts", {
374
+ method: "POST",
375
+ body: JSON.stringify({ name: name.trim(), ...payload }),
376
+ signal: options?.signal,
377
+ }));
378
+ if (!item) throw new Error("The layout marketplace returned invalid data.");
379
+ return item;
380
+ }
381
+
324
382
  async updateSyncSettings(update: Partial<SyncSettings>): Promise<SyncSettings> {
325
383
  const result = await this.request<{ settings: SyncSettings }>("/sync/settings", {
326
384
  method: "PATCH",
@@ -488,6 +546,10 @@ class GloomApiClient {
488
546
  return this.data.getCloudQuotesBatch(targets, mode);
489
547
  }
490
548
 
549
+ async getCloudWorldVenues(): Promise<CloudMarketResponse<CloudWorldVenueMapPayload>> {
550
+ return this.data.getCloudWorldVenues();
551
+ }
552
+
491
553
  async getCloudMarketScreener(
492
554
  category: CloudMarketScreenerCategory,
493
555
  count = 25,
@@ -582,6 +644,10 @@ class GloomApiClient {
582
644
  return this.data.getCloudYieldCurve();
583
645
  }
584
646
 
647
+ async getCloudCds(params: CloudCdsParams = {}): Promise<CloudCdsResponse> {
648
+ return this.data.getCloudCds(params);
649
+ }
650
+
585
651
  async getCloudCongressHouse(params: CloudCongressHouseParams = {}): Promise<CloudCongressHousePayload> {
586
652
  return this.data.getCloudCongressHouse(params);
587
653
  }
@@ -16,6 +16,13 @@ export type CloudFredSeriesParams = {
16
16
  sortOrder?: "asc" | "desc";
17
17
  };
18
18
 
19
+ export type CloudCdsParams = {
20
+ /** Reference entity name, matched by the backend against DTCC issuer names. */
21
+ issuer?: string;
22
+ days?: number;
23
+ limit?: number;
24
+ };
25
+
19
26
  export type CloudCongressHouseParams = {
20
27
  year?: number;
21
28
  limit?: number;
@@ -137,6 +144,15 @@ export function cloudFredSeriesPath(seriesId: string, params: CloudFredSeriesPar
137
144
  return appendQuery(`/cloud/econ/series/${encodeURIComponent(seriesId)}`, search);
138
145
  }
139
146
 
147
+ export function cloudCdsPath(params: CloudCdsParams = {}): string {
148
+ const search = new URLSearchParams();
149
+ const issuer = params.issuer?.trim();
150
+ if (issuer) search.set("issuer", issuer);
151
+ if (params.days != null) search.set("days", String(params.days));
152
+ if (params.limit != null) search.set("limit", String(params.limit));
153
+ return appendQuery("/cloud/credit/cds", search);
154
+ }
155
+
140
156
  export type CloudSecFilingsParams = {
141
157
  ticker: string;
142
158
  limit?: number;
@@ -253,6 +253,30 @@ export interface CloudQuotePayload extends Quote {
253
253
  dataSource: "live" | "delayed";
254
254
  }
255
255
 
256
+ export interface CloudWorldVenuePayload {
257
+ mic: string;
258
+ name: string;
259
+ title: string;
260
+ country: string;
261
+ countryCode: string;
262
+ city: string;
263
+ timezone: string;
264
+ latitude: number;
265
+ longitude: number;
266
+ isOpen: boolean;
267
+ timeAfterOpenSeconds?: number;
268
+ timeToOpenSeconds?: number;
269
+ timeToCloseSeconds?: number;
270
+ }
271
+
272
+ export interface CloudWorldVenueMapPayload {
273
+ providerId: "gloomberb-cloud";
274
+ checkedAt: number;
275
+ refreshAt: number;
276
+ stale?: boolean;
277
+ venues: CloudWorldVenuePayload[];
278
+ }
279
+
256
280
  export interface CloudOptionsChainPayload extends OptionsChain {
257
281
  providerId: "gloomberb-cloud";
258
282
  }
@@ -322,6 +346,40 @@ export interface CloudFredSeriesPayload {
322
346
  info: CloudFredSeriesInfoPayload | null;
323
347
  }
324
348
 
349
+ /** One publicly disseminated single-name CDS transaction report. */
350
+ export interface CloudCdsTradePayload {
351
+ disseminationId: string;
352
+ originalDisseminationId: string | null;
353
+ actionType: string;
354
+ eventTimestamp: string;
355
+ executionTimestamp: string | null;
356
+ effectiveDate: string | null;
357
+ expirationDate: string | null;
358
+ maturityDate: string | null;
359
+ issuerName: string | null;
360
+ underlierId: string | null;
361
+ underlierIdSource: string | null;
362
+ upi: string | null;
363
+ upiFisn: string | null;
364
+ upiUnderlierName: string | null;
365
+ notionalAmount: number | null;
366
+ /** Reported notional is a regulatory cap, so the real trade was at least this size. */
367
+ notionalCapped: boolean;
368
+ notionalCurrency: string | null;
369
+ fixedRate: number | null;
370
+ reportedSpread: number | null;
371
+ /** Unit of `reportedSpread`, e.g. basis points or percent. */
372
+ spreadNotation: string | null;
373
+ upfrontAmount: number | null;
374
+ upfrontCurrency: string | null;
375
+ }
376
+
377
+ export interface CloudCdsResponse {
378
+ source: string;
379
+ asOf: string | null;
380
+ trades: CloudCdsTradePayload[];
381
+ }
382
+
325
383
  export interface CloudShortInterestPointPayload {
326
384
  settlementDate: string;
327
385
  sharesShort: number;
@@ -720,6 +778,7 @@ export interface CloudEquityDiagnosticPending {
720
778
 
721
779
  export interface CloudEquityDiagnosticResponse {
722
780
  schemaVersion: 1;
781
+ access: "preview" | "full";
723
782
  symbol: string;
724
783
  exchange: string;
725
784
  companyName?: string;
@@ -11,6 +11,7 @@ import {
11
11
  import type { PluginRegistry } from "../../plugins/registry";
12
12
  import {
13
13
  getFocusedCollectionId,
14
+ resolveTickerForPane,
14
15
  syncConfigActiveLayoutState,
15
16
  type AppAction,
16
17
  type AppState,
@@ -75,7 +76,9 @@ export function useAppPaneRuntime({
75
76
 
76
77
  const persistLayout = useCallback((layout: LayoutConfig, options?: { pushHistory?: boolean; focusedPaneId?: string | null }) => {
77
78
  const currentState = stateRef.current;
78
- const normalizedLayout = normalizePaneLayout(layout);
79
+ const normalizedLayout = normalizePaneLayout(layout, {
80
+ resolveOrphanSymbol: (instanceId) => resolveTickerForPane(currentState, instanceId),
81
+ });
79
82
  if (options?.pushHistory !== false) {
80
83
  dispatch({ type: "PUSH_LAYOUT_HISTORY" });
81
84
  }
@@ -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
  }