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
@@ -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
 
package/src/app.tsx CHANGED
@@ -54,6 +54,7 @@ import { scheduleConfigSave } from "./state/config-save-scheduler";
54
54
  import { measurePerf } from "./utils/perf-marks";
55
55
  import { useAppLanguage } from "./i18n/react";
56
56
  import { AppLanguageConfigObserver } from "./app/language-observer";
57
+ import { isPaneShareHandoff } from "./shares/location";
57
58
 
58
59
  const EMPTY_EXTERNAL_PLUGINS: LoadedExternalPlugin[] = [];
59
60
 
@@ -158,7 +159,9 @@ function AppInner({
158
159
  const type = notification.type ?? "info";
159
160
  let toastId: string | number | undefined;
160
161
  const options = {
161
- duration: notification.duration,
162
+ title: notification.title,
163
+ subtitle: notification.subtitle,
164
+ duration: notification.persistent ? 0 : notification.duration,
162
165
  action: notification.action
163
166
  ? {
164
167
  label: notification.action.label,
@@ -246,6 +249,7 @@ function AppInner({
246
249
  desktopDeepLinkBridge,
247
250
  desktopWindowKind: desktopWindowBridge?.kind,
248
251
  dispatch,
252
+ initialized: state.initialized,
249
253
  pluginRegistry,
250
254
  stateRef,
251
255
  });
@@ -275,7 +279,7 @@ function AppInner({
275
279
  importBrokerPositions,
276
280
  marketData,
277
281
  pluginRegistry,
278
- state,
282
+ stateRef,
279
283
  tickerRepository,
280
284
  });
281
285
 
@@ -285,6 +289,7 @@ function AppInner({
285
289
  dispatch,
286
290
  tickerRepository,
287
291
  pluginRegistry,
292
+ appActive,
288
293
  // Keep a first-run workspace stable while the local guide is active. Once
289
294
  // onboarding finishes, the normal pull-before-push sync starts immediately.
290
295
  initialized: state.initialized
@@ -464,8 +469,10 @@ export function App({
464
469
  const [config, setConfig] = useState(() => {
465
470
  return initialCliLaunch.config;
466
471
  });
472
+ const shareHandoff = isPaneShareHandoff();
467
473
  const [showOnboarding, setShowOnboarding] = useState(() => (
468
474
  desktopWindowBridge?.kind !== "detached"
475
+ && !shareHandoff
469
476
  && (!effectiveInitialConfig.onboardingComplete || !!effectiveInitialConfig.onboardingProgress)
470
477
  ));
471
478
 
@@ -9,6 +9,7 @@ import type {
9
9
  SeriesTransform,
10
10
  TimeSeriesPoint,
11
11
  } from "../time-series/types";
12
+ import { CHART_RESOLUTIONS, TIME_RANGES } from "../time-series/range";
12
13
  import type {
13
14
  CapabilityInvoker,
14
15
  CapabilitySchema,
@@ -24,8 +25,8 @@ export const MAX_CHART_SERIES_POINTS = 20_000;
24
25
  const CAPABILITY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
25
26
  const SERIES_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~%:/-]{0,239}$/;
26
27
  const chartSeriesLog = debugLog.createLogger("chart-series");
27
- const ranges = new Set(["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"]);
28
- const resolutions = new Set(["auto", "1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"]);
28
+ const ranges = new Set<string>(TIME_RANGES);
29
+ const resolutions = new Set<string>(CHART_RESOLUTIONS);
29
30
  const periods = new Set(["auto", "daily", "weekly", "monthly", "quarterly", "annual", "ttm"]);
30
31
  const styles = new Set(["line", "area", "step", "columns", "points", "candles", "ohlc", "hlc"]);
31
32
  const transforms = new Set(["raw", "percent", "index100", "yoy", "qoq", "log"]);
@@ -199,4 +199,13 @@ describe("headless Pi AI command", () => {
199
199
  expect(disconnected.runs).toEqual([]);
200
200
  expect(disconnected.closed()).toBe(1);
201
201
  });
202
+
203
+ test("rejects unsupported verbs instead of returning a stub payload", async () => {
204
+ const harness = createHarness({
205
+ catalog: { providers: [], refreshErrors: {} },
206
+ });
207
+ await expect(harness.command.execute(["screen"], harness.context))
208
+ .rejects.toThrow("Usage: gloomberb ai providers|ask");
209
+ expect(harness.output).toEqual([]);
210
+ });
202
211
  });