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
@@ -179,17 +179,17 @@ describe("CommandBar", () => {
179
179
  expect(testSetup.captureCharFrame()).not.toContain("GitHub Light");
180
180
  });
181
181
 
182
- test("runs plugin command shortcuts from the root query", async () => {
182
+ test("finds the tidy windows command by its snap alias", async () => {
183
183
  const calls: string[] = [];
184
184
 
185
185
  testSetup = await testRender(<CommandBarHarness
186
- query="GL"
186
+ query="snap"
187
187
  configurePluginRegistry={(pluginRegistry) => {
188
188
  (pluginRegistry.commands as Map<string, any>).set("gridlock-all", {
189
189
  id: "gridlock-all",
190
- label: "Gridlock All Windows",
191
- description: "Arrange all visible panes into a tiled grid",
192
- keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
190
+ label: "Tidy Windows",
191
+ description: "Arrange every window into one tiled layout",
192
+ keywords: ["tidy", "snap", "grid", "gridlock", "tile", "arrange", "organize", "organise", "cleanup", "dock", "floating", "windows", "layout"],
193
193
  shortcut: "GL",
194
194
  category: "config",
195
195
  execute: async () => {
@@ -215,7 +215,7 @@ describe("CommandBar", () => {
215
215
  await testSetup.renderOnce();
216
216
 
217
217
  const frame = testSetup.captureCharFrame();
218
- expect(frame).toContain("Gridlock All Windows");
218
+ expect(frame).toContain("Tidy Windows");
219
219
  expect(frame).toContain("GL");
220
220
 
221
221
  await act(async () => {
@@ -142,6 +142,23 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
142
142
  });
143
143
  }
144
144
 
145
+ function registerOptionalTickerPane(pluginRegistry: MutablePaneRegistry): void {
146
+ mutableRegistryMap(pluginRegistry.panes).set("optional-ticker", {
147
+ id: "optional-ticker",
148
+ name: "Optional Ticker",
149
+ component: () => null,
150
+ defaultPosition: "right",
151
+ defaultMode: "floating",
152
+ });
153
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("optional-ticker-pane", {
154
+ id: "optional-ticker-pane",
155
+ paneId: "optional-ticker",
156
+ label: "Optional Ticker",
157
+ description: "Open market-wide or for one ticker.",
158
+ shortcut: { prefix: "OT", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
159
+ });
160
+ }
161
+
145
162
  function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
146
163
  mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
147
164
  id: "prediction-markets",
@@ -204,7 +221,7 @@ describe("CommandBar pane and layout routes", () => {
204
221
 
205
222
  test("renders layout mode with focused pane actions", async () => {
206
223
  testSetup = await testRender(<CommandBarHarness
207
- query="LAY "
224
+ query="LMA "
208
225
  configureConfig={layoutModeConfig}
209
226
  configureState={layoutModeState}
210
227
  />, {
@@ -248,9 +265,29 @@ describe("CommandBar pane and layout routes", () => {
248
265
  expect(actions.some((action) => action.type === "UNDO_LAYOUT")).toBe(true);
249
266
  });
250
267
 
268
+ test("opens Layouts as a normal pane from LAY", async () => {
269
+ const opened: string[] = [];
270
+ testSetup = await testRender(
271
+ <CommandBarHarness
272
+ query="LAY"
273
+ live
274
+ configurePluginRegistry={(pluginRegistry) => {
275
+ pluginRegistry.showPane = (paneId) => opened.push(paneId);
276
+ }}
277
+ />,
278
+ { width: 90, height: 18 },
279
+ );
280
+ await testSetup.renderOnce();
281
+ expect(testSetup.captureCharFrame()).toContain("Layouts");
282
+
283
+ await clickFrameText("Layouts");
284
+
285
+ expect(opened).toEqual(["layout-marketplace"]);
286
+ });
287
+
251
288
  test("renders filtered saved layouts with textual previews", async () => {
252
289
  testSetup = await testRender(<CommandBarHarness
253
- query="LAY Research"
290
+ query="LMA Research"
254
291
  configureConfig={layoutModeConfig}
255
292
  configureState={layoutModeState}
256
293
  />, {
@@ -353,6 +390,33 @@ describe("CommandBar pane and layout routes", () => {
353
390
  expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
354
391
  });
355
392
 
393
+ test("keeps an optional ticker shortcut market-wide when a ticker is active", async () => {
394
+ const created: CreatedPaneCall[] = [];
395
+
396
+ testSetup = await testRender(<CommandBarHarness
397
+ query="OT"
398
+ selectedTicker="AAPL"
399
+ live
400
+ configurePluginRegistry={(pluginRegistry) => {
401
+ registerOptionalTickerPane(pluginRegistry);
402
+ recordPaneCreations(pluginRegistry, created);
403
+ }}
404
+ />, {
405
+ width: 100,
406
+ height: 18,
407
+ });
408
+
409
+ await testSetup.renderOnce();
410
+
411
+ await act(async () => {
412
+ testSetup!.mockInput.pressEnter();
413
+ await Bun.sleep(0);
414
+ await testSetup!.renderOnce();
415
+ });
416
+
417
+ expect(created).toEqual([{ templateId: "optional-ticker-pane", options: undefined }]);
418
+ });
419
+
356
420
  for (const scenario of [
357
421
  {
358
422
  query: "prediction markets",
@@ -14,7 +14,8 @@ describe("command bar view model helpers", () => {
14
14
  expect(resolveCommandBarMode("T NVDA")).toMatchObject({ kind: "search", badge: "T" });
15
15
  expect(resolveCommandBarMode("TH ")).toMatchObject({ kind: "themes", badge: "THEMES" });
16
16
  expect(resolveCommandBarMode("PL notes")).toMatchObject({ kind: "plugins", badge: "PLUGINS" });
17
- expect(resolveCommandBarMode("LAY ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
17
+ expect(resolveCommandBarMode("LAY")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
18
+ expect(resolveCommandBarMode("LMA ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
18
19
  expect(resolveCommandBarMode("NP ")).toMatchObject({ kind: "default", badge: "FILTER" });
19
20
  expect(resolveCommandBarMode("PS")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
20
21
  expect(resolveCommandBarMode("AW")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
@@ -3,6 +3,7 @@ import { cloneLayout, createDefaultConfig, findPaneInstance, type LayoutConfig }
3
3
  import { createInitialState } from "../../../state/app/context";
4
4
  import { createTestDataProvider } from "../../../test-support/data-provider";
5
5
  import { applyPaneSettingFieldValue, createPaneTemplateOrThrow } from "./ops";
6
+ import type { TickerRecord } from "../../../types/ticker";
6
7
 
7
8
  function makeDataProvider() {
8
9
  return createTestDataProvider({ id: "test" });
@@ -68,6 +69,59 @@ describe("createPaneTemplateOrThrow", () => {
68
69
  expect(placeCalls).toHaveLength(0);
69
70
  });
70
71
 
72
+ test("uses an explicit shared symbol instead of the recipient's active ticker", async () => {
73
+ const config = createDefaultConfig("/tmp/gloomberb-workflow-ops-test");
74
+ const state = createInitialState(config);
75
+ const msft: TickerRecord = {
76
+ metadata: {
77
+ ticker: "MSFT",
78
+ exchange: "NASDAQ",
79
+ currency: "USD",
80
+ name: "Microsoft",
81
+ portfolios: [],
82
+ watchlists: [],
83
+ positions: [],
84
+ broker_contracts: [],
85
+ custom: {},
86
+ tags: [],
87
+ },
88
+ };
89
+ state.tickers.set("MSFT", msft);
90
+ let createdSymbol: string | null | undefined;
91
+
92
+ await createPaneTemplateOrThrow("ticker-pane", { symbol: "MSFT" }, {
93
+ dataProvider: makeDataProvider() as any,
94
+ tickerRepository: makeTickerRepository() as any,
95
+ dispatch: () => {},
96
+ getState: () => state,
97
+ pluginRegistry: {
98
+ paneTemplates: new Map([["ticker-pane", {
99
+ id: "ticker-pane",
100
+ paneId: "ticker-view",
101
+ label: "Ticker View",
102
+ description: "Ticker View",
103
+ shortcut: { prefix: "TV", argPlaceholder: "ticker", argKind: "ticker" },
104
+ createInstance: (_context: unknown, options: { symbol?: string } | undefined) => {
105
+ createdSymbol = options?.symbol;
106
+ return { binding: { kind: "fixed", symbol: options?.symbol ?? "" } };
107
+ },
108
+ }]]),
109
+ panes: new Map([["ticker-view", {
110
+ id: "ticker-view",
111
+ name: "Ticker View",
112
+ component: () => null,
113
+ defaultPosition: "right",
114
+ }]]),
115
+ getPaneTemplatePluginId: () => undefined,
116
+ events: { emit: () => {} },
117
+ } as any,
118
+ buildPaneInstance: () => ({ instanceId: "ticker-view:1", paneId: "ticker-view" }) as any,
119
+ placePaneInstance: () => {},
120
+ });
121
+
122
+ expect(createdSymbol).toBe("MSFT");
123
+ });
124
+
71
125
  test("passes pane template instance ids through to pane creation", async () => {
72
126
  const config = createDefaultConfig("/tmp/gloomberb-workflow-ops-test");
73
127
  const state = createInitialState(config);
@@ -152,7 +152,7 @@ async function resolvePaneTemplateOptions(
152
152
  let resolvedOptions = options;
153
153
  if (template.shortcut?.argPlaceholder === "ticker") {
154
154
  const resolvedTicker = await resolveTickerInputOrThrow(
155
- resolvedOptions?.arg,
155
+ resolvedOptions?.symbol ?? resolvedOptions?.arg,
156
156
  baseContext.activeTicker,
157
157
  baseContext.activeCollectionId,
158
158
  deps,
@@ -0,0 +1,21 @@
1
+ import { expect, test } from "bun:test";
2
+ import { EXCEL_CSV_BOM } from "../../utils/csv";
3
+ import { createDataTableCsv } from "./export";
4
+
5
+ test("exports displayed table cells and skips section headers", () => {
6
+ const items = [
7
+ { type: "section", name: "Americas", value: "" },
8
+ { type: "row", name: "S&P 500", value: "+1.25%" },
9
+ ];
10
+ const csv = createDataTableCsv({
11
+ columns: [
12
+ { id: "name", label: "Name", width: 20, align: "left" },
13
+ { id: "value", label: "Change", width: 10, align: "right" },
14
+ ],
15
+ items,
16
+ renderSectionHeader: (item) => item.type === "section" ? { text: item.name } : null,
17
+ renderCell: (item, column) => ({ text: item[column.id as "name" | "value"] }),
18
+ });
19
+
20
+ expect(csv).toBe(`${EXCEL_CSV_BOM}Name,Change\nS&P 500,'+1.25%`);
21
+ });
@@ -0,0 +1,21 @@
1
+ import type { DataTableColumn, DataTableProps } from "../ui";
2
+ import { serializeCsv } from "../../utils/csv";
3
+
4
+ export function createDataTableCsv<
5
+ T,
6
+ C extends DataTableColumn = DataTableColumn,
7
+ >({
8
+ columns,
9
+ items,
10
+ renderCell,
11
+ renderSectionHeader,
12
+ }: Pick<DataTableProps<T, C>, "columns" | "items" | "renderCell" | "renderSectionHeader">): string {
13
+ const rows = items.flatMap((item, index) => {
14
+ if (renderSectionHeader?.(item, index)) return [];
15
+ return [columns.map((column) => renderCell(item, column, index, { selected: false }).text)];
16
+ });
17
+ return serializeCsv([
18
+ columns.map((column) => column.label),
19
+ ...rows,
20
+ ], { excelCompatible: true });
21
+ }
@@ -1,8 +1,8 @@
1
- import { Box, Span, Text, useRendererHost, useUiCapabilities } from "../../ui";
1
+ import { Box, Span, Text, useContextMenu, useRendererHost, useUiCapabilities } from "../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { t } from "../../i18n";
4
4
  import { useShortcut, useViewport } from "../../react/input";
5
- import { useAppDispatch, useAppSelector } from "../../state/app/context";
5
+ import { resolveTickerForPane, useAppDispatch, useAppSelector } from "../../state/app/context";
6
6
  import type { DesktopWindowBridge } from "../../types/desktop-window";
7
7
  import { findPaneInstance } from "../../types/config";
8
8
  import type { PluginRegistry } from "../../plugins/registry";
@@ -20,6 +20,13 @@ import {
20
20
  recordDoubleEscapeClose,
21
21
  resetDoubleEscapeClose,
22
22
  } from "../../utils/double-escape-close";
23
+ import { createShare, openLiveShareUrl } from "../../shares/api";
24
+ import { buildPaneSharePayload } from "../../shares/pane";
25
+ import type { ContextMenuItem } from "../../types/context-menu";
26
+ import {
27
+ PANE_MANAGEMENT_ACCELERATORS,
28
+ resolvePaneManagementShortcut,
29
+ } from "./shell/shortcuts";
23
30
 
24
31
  interface DetachedPaneShellProps {
25
32
  pluginRegistry: PluginRegistry;
@@ -35,6 +42,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
35
42
  const colors = useThemeColors();
36
43
  const dispatch = useAppDispatch();
37
44
  const rendererHost = useRendererHost();
45
+ const { showContextMenu } = useContextMenu();
38
46
  const config = useAppSelector((state) => state.config);
39
47
  const paneState = useAppSelector((state) => state.paneState);
40
48
  const inputCaptured = useAppSelector((state) => state.inputCaptured);
@@ -46,6 +54,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
46
54
  const {
47
55
  cellHeightPx = 18,
48
56
  nativePaneChrome,
57
+ publicSharing,
49
58
  titleBarOverlay,
50
59
  nativeWindowChrome = titleBarOverlay,
51
60
  windowControls,
@@ -55,13 +64,21 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
55
64
  const instance = useAppSelector((state) => findPaneInstance(state.config.layout, desktopWindowBridge.paneId) ?? null);
56
65
  const paneDef = instance ? pluginRegistry.panes.get(instance.paneId) ?? null : null;
57
66
  const hasPaneSettings = !!instance && pluginRegistry.hasPaneSettings(instance.instanceId);
58
- const quickSettings = instance ? pluginRegistry.resolvePaneQuickSettings(instance.instanceId) : [];
59
67
  const titleState = useMemo(
60
- () => ({ config, paneState }) as Parameters<typeof getPaneDisplayTitle>[0],
68
+ () => ({ config, paneState }) as Parameters<typeof resolveTickerForPane>[0],
61
69
  [config, paneState],
62
70
  );
71
+ const sharePayload = useMemo(() => instance && publicSharing
72
+ ? buildPaneSharePayload(
73
+ pluginRegistry,
74
+ instance,
75
+ paneState[instance.instanceId] ?? {},
76
+ resolveTickerForPane(titleState, instance.instanceId),
77
+ )
78
+ : null, [instance, paneState, pluginRegistry, publicSharing, titleState]);
79
+ const quickSettings = instance ? pluginRegistry.resolvePaneQuickSettings(instance.instanceId) : [];
63
80
  const title = instance && paneDef
64
- ? getPaneDisplayTitle(titleState, instance, paneDef)
81
+ ? getPaneDisplayTitle(titleState, instance, paneDef, pluginRegistry.panes)
65
82
  : "Detached Pane";
66
83
  const focused = windowFocused;
67
84
 
@@ -121,10 +138,56 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
121
138
  if (nativeWindowChrome) void rendererHost.startWindowDrag?.();
122
139
  }, [focusPane, nativeWindowChrome, rendererHost]);
123
140
 
124
- const openSettings = useCallback((event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
141
+ const sharePane = useCallback(async () => {
142
+ if (!sharePayload) return;
143
+ try {
144
+ const { id } = await createShare(sharePayload);
145
+ await rendererHost.copyText(openLiveShareUrl(id));
146
+ pluginRegistry.notify({ body: "Share link copied to clipboard", type: "success" });
147
+ } catch (error) {
148
+ pluginRegistry.notify({
149
+ body: error instanceof Error ? error.message : "Could not share this pane.",
150
+ type: "error",
151
+ });
152
+ }
153
+ }, [pluginRegistry, rendererHost, sharePayload]);
154
+
155
+ useShortcut((event) => {
156
+ if (resolvePaneManagementShortcut(event) !== "share" || !sharePayload) return;
157
+ if (inputCaptured && event.ctrl && !event.meta && !event.super) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ void sharePane();
161
+ });
162
+
163
+ const openActions = useCallback((event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
125
164
  stopMouse(event);
126
- pluginRegistry.openPaneSettingsFn(desktopWindowBridge.paneId);
127
- }, [desktopWindowBridge.paneId, pluginRegistry]);
165
+ focusPane();
166
+ const items: ContextMenuItem[] = [];
167
+ if (hasPaneSettings) {
168
+ items.push({
169
+ id: "settings",
170
+ label: "Settings",
171
+ onSelect: () => pluginRegistry.openPaneSettingsFn(desktopWindowBridge.paneId),
172
+ });
173
+ }
174
+ if (sharePayload) items.push({
175
+ id: "share-pane",
176
+ label: "Share Pane",
177
+ accelerator: PANE_MANAGEMENT_ACCELERATORS.share,
178
+ onSelect: sharePane,
179
+ });
180
+ void showContextMenu({
181
+ kind: "pane",
182
+ paneId: desktopWindowBridge.paneId,
183
+ paneType: instance?.paneId ?? "",
184
+ title,
185
+ floating: true,
186
+ }, items, event).then((shown) => {
187
+ if (!shown && hasPaneSettings) pluginRegistry.openPaneSettingsFn(desktopWindowBridge.paneId);
188
+ else if (!shown && sharePayload) void sharePane();
189
+ });
190
+ }, [desktopWindowBridge.paneId, focusPane, hasPaneSettings, instance?.paneId, pluginRegistry, sharePane, sharePayload, showContextMenu, title]);
128
191
  const toggleQuickSetting = useCallback((key: string, event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
129
192
  stopMouse(event);
130
193
  focusPane();
@@ -230,14 +293,15 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
230
293
  </Box>
231
294
  ))}
232
295
  <Box flexGrow={1} minWidth={0} />
233
- {hasPaneSettings && (
296
+ {(hasPaneSettings || sharePayload) && (
234
297
  <Text
235
298
  fg={paneTitleText(focused, true, colors)}
236
299
  selectable={false}
237
300
  className="electrobun-webkit-app-region-no-drag"
238
301
  data-gloom-role="pane-action"
239
302
  data-gloom-interactive="true"
240
- onMouseDown={openSettings}
303
+ aria-label="Pane actions"
304
+ onMouseDown={openActions}
241
305
  >
242
306
  {" ... "}
243
307
  </Text>
@@ -1,9 +1,16 @@
1
1
  import { resolveCollectionForPane, resolveTickerForPane, type AppState } from "../../../state/app/context";
2
- import { t } from "../../../i18n";
3
- import { TICKER_RESEARCH_PANE_ID, type PaneInstanceConfig } from "../../../types/config";
2
+ import { t, tf } from "../../../i18n";
3
+ import {
4
+ findPaneInstance,
5
+ TICKER_RESEARCH_PANE_ID,
6
+ type PaneInstanceConfig,
7
+ } from "../../../types/config";
4
8
  import type { PaneDef } from "../../../types/plugin";
5
9
 
6
- export function getPaneDisplayTitle(
10
+ /** Single-cell link glyph: emoji link icons render double-width in terminals. */
11
+ const LINK_GLYPH = "\u29c9";
12
+
13
+ function getBasePaneDisplayTitle(
7
14
  state: Pick<AppState, "config" | "paneState">,
8
15
  instance: PaneInstanceConfig,
9
16
  paneDef: PaneDef,
@@ -38,6 +45,27 @@ export function getPaneDisplayTitle(
38
45
  ?? t(paneDef.name);
39
46
  }
40
47
 
48
+ // A source pane owns the cursor symbol; echoing it in its own title would just repeat the row.
49
+ if (paneDef.tickerSource) return t(paneDef.name);
50
+
41
51
  const ticker = resolveTickerForPane(state as AppState, instance.instanceId);
42
52
  return ticker ? `${t(paneDef.name)}: ${ticker}` : t(paneDef.name);
43
53
  }
54
+
55
+ export function getPaneDisplayTitle(
56
+ state: Pick<AppState, "config" | "paneState">,
57
+ instance: PaneInstanceConfig,
58
+ paneDef: PaneDef,
59
+ panes?: ReadonlyMap<string, PaneDef>,
60
+ ): string {
61
+ const title = getBasePaneDisplayTitle(state, instance, paneDef);
62
+ if (instance.paneId !== TICKER_RESEARCH_PANE_ID || instance.binding?.kind !== "follow" || !panes) {
63
+ return title;
64
+ }
65
+
66
+ const source = findPaneInstance(state.config.layout, instance.binding.sourceInstanceId);
67
+ const sourceDef = source ? panes.get(source.paneId) : null;
68
+ if (!source || !sourceDef) return title;
69
+ const sourceTitle = getBasePaneDisplayTitle(state, source, sourceDef);
70
+ return `${title} ${LINK_GLYPH} ${tf("Linked to {source}", { source: sourceTitle })}`;
71
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, type Dispatch } from "react";
1
+ import { useCallback } from "react";
2
2
  import {
3
3
  floatAtRect,
4
4
  getRememberedFloatingRect,
@@ -10,7 +10,6 @@ import {
10
10
  type LayoutBounds,
11
11
  type ResolvedPane,
12
12
  } from "../../../plugins/pane-manager";
13
- import type { AppAction } from "../../../state/app/context";
14
13
  import type { LayoutConfig } from "../../../types/config";
15
14
  import {
16
15
  finalizePaneDragRelease,
@@ -32,7 +31,6 @@ interface UseShellActiveDragOptions {
32
31
  appHeaderHeight: number;
33
32
  bounds: LayoutBounds;
34
33
  contentHeight: number;
35
- dispatch: Dispatch<AppAction>;
36
34
  dockGeometryOptions: DockGeometryOptions;
37
35
  dockLeafLayouts: DockLeafLayout[];
38
36
  dragRuntime: ShellDragRuntimeState;
@@ -52,7 +50,6 @@ export function useShellActiveDrag({
52
50
  appHeaderHeight,
53
51
  bounds,
54
52
  contentHeight,
55
- dispatch,
56
53
  dockGeometryOptions,
57
54
  dockLeafLayouts,
58
55
  dragRuntime,
@@ -168,16 +165,13 @@ export function useShellActiveDrag({
168
165
  ? getRememberedFloatingRect(baseLayout, drag.paneId, width, contentHeight, pane?.def)
169
166
  : drag.origRect;
170
167
  const releaseRect = resolvePaneDragFloatingRect(drag, baseRect, preciseX, preciseShellY, width, contentHeight);
171
- const releaseResult = finalizePaneDragRelease(baseLayout, drag.paneId, releaseRect, dockPreviewRef.current);
168
+ const nextLayout = finalizePaneDragRelease(baseLayout, drag.paneId, releaseRect, dockPreviewRef.current);
172
169
  if (windowMode) {
173
- updateWindowModePreviewLayout(releaseResult.nextLayout, drag.paneId);
170
+ updateWindowModePreviewLayout(nextLayout, drag.paneId);
174
171
  } else {
175
- persistLayout(releaseResult.nextLayout);
172
+ persistLayout(nextLayout);
176
173
  }
177
174
  focusPane(drag.paneId);
178
- if (!windowMode && releaseResult.shouldShowGridlockTip) {
179
- dispatch({ type: "SHOW_GRIDLOCK_TIP" });
180
- }
181
175
  updateDockPreview(null);
182
176
  setDragCursor(null);
183
177
  updateDragFloatingRect(null);
@@ -201,7 +195,6 @@ export function useShellActiveDrag({
201
195
  appHeaderHeight,
202
196
  bounds,
203
197
  contentHeight,
204
- dispatch,
205
198
  dockGeometryOptions,
206
199
  dividerPreviewRef,
207
200
  dockLeafLayouts,
@@ -46,11 +46,6 @@ export type DragPreview =
46
46
  rect: FloatingRect;
47
47
  };
48
48
 
49
- export interface PaneDragReleaseResult {
50
- nextLayout: LayoutConfig;
51
- shouldShowGridlockTip: boolean;
52
- }
53
-
54
49
  export interface PaneDragRectState {
55
50
  mode: "docked" | "floating";
56
51
  startX: number;
@@ -272,25 +267,14 @@ export function finalizePaneDragRelease(
272
267
  paneId: string,
273
268
  previewRect: FloatingRect,
274
269
  dockPreview: DragPreview | null,
275
- ): PaneDragReleaseResult {
270
+ ): LayoutConfig {
276
271
  if (dockPreview?.kind === "dock") {
277
- return {
278
- nextLayout: applyDrop(layout, paneId, dockPreview.target),
279
- shouldShowGridlockTip: false,
280
- };
272
+ return applyDrop(layout, paneId, dockPreview.target);
281
273
  }
282
-
283
274
  if (dockPreview?.kind === "snap") {
284
- return {
285
- nextLayout: floatAtRect(layout, paneId, dockPreview.rect),
286
- shouldShowGridlockTip: true,
287
- };
275
+ return floatAtRect(layout, paneId, dockPreview.rect);
288
276
  }
289
-
290
- return {
291
- nextLayout: floatAtRect(layout, paneId, previewRect),
292
- shouldShowGridlockTip: false,
293
- };
277
+ return floatAtRect(layout, paneId, previewRect);
294
278
  }
295
279
 
296
280
  export function makeSnapGuides(width: number, height: number): SnapGuide[] {
@@ -15,7 +15,6 @@ import {
15
15
  type LayoutBounds,
16
16
  type ResolvedPane,
17
17
  } from "../../../../plugins/pane-manager";
18
- import type { AppAction } from "../../../../state/app/context";
19
18
  import type { LayoutConfig } from "../../../../types/config";
20
19
  import {
21
20
  constrainFloatingRectToBounds,
@@ -165,7 +164,6 @@ interface UseShellPointerRuntimeOptions {
165
164
  bounds: LayoutBounds;
166
165
  closePaneMenu: () => void;
167
166
  contentHeight: number;
168
- dispatch: Dispatch<AppAction>;
169
167
  dockGeometryOptions: DockGeometryOptions;
170
168
  dockDividerLayouts: DockDividerLayout[];
171
169
  dockLeafLayouts: DockLeafLayout[];
@@ -193,6 +191,7 @@ interface UseShellPointerRuntimeOptions {
193
191
  visibleLayout: LayoutConfig;
194
192
  width: number;
195
193
  windowMode: WindowEditState | null;
194
+ commandBarOpen: boolean;
196
195
  }
197
196
 
198
197
  export function useShellPointerRuntime({
@@ -200,7 +199,6 @@ export function useShellPointerRuntime({
200
199
  bounds,
201
200
  closePaneMenu,
202
201
  contentHeight,
203
- dispatch,
204
202
  dockGeometryOptions,
205
203
  dockDividerLayouts,
206
204
  dockLeafLayouts,
@@ -224,12 +222,12 @@ export function useShellPointerRuntime({
224
222
  visibleLayout,
225
223
  width,
226
224
  windowMode,
225
+ commandBarOpen,
227
226
  }: UseShellPointerRuntimeOptions) {
228
227
  const handleActiveDrag = useShellActiveDrag({
229
228
  appHeaderHeight,
230
229
  bounds,
231
230
  contentHeight,
232
- dispatch,
233
231
  dockGeometryOptions,
234
232
  dockLeafLayouts,
235
233
  focusPane,
@@ -282,6 +280,7 @@ export function useShellPointerRuntime({
282
280
  setMenuState,
283
281
  transientFocusActive,
284
282
  windowMode,
283
+ commandBarOpen,
285
284
  });
286
285
 
287
286
  return {
@@ -62,6 +62,7 @@ function createShellPluginRegistry(options?: {
62
62
  hasPaneSettings: (paneId: string) => paneId === "portfolio-list:main",
63
63
  openPaneSettingsFn: () => {},
64
64
  openCommandBar: () => {},
65
+ showPane: () => {},
65
66
  openWindowMode: () => {},
66
67
  openWindowModeFn: () => {},
67
68
  updateLayoutFn: () => {},
@@ -356,7 +357,8 @@ describe("Shell", () => {
356
357
  expect(resolvePaneManagementShortcut({ ...base, name: "D", key: "D" })).toBe("toggle-floating");
357
358
  expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
358
359
  expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
359
- expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
360
+ expect(resolvePaneManagementShortcut({ ...base, name: "s", key: "s" })).toBe("share");
361
+ expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-gallery");
360
362
  expect(resolvePaneManagementShortcut({ ...base, name: "f", key: "f" })).toBe("toggle-fullscreen");
361
363
  expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
362
364
  expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode");
@@ -366,6 +368,18 @@ describe("Shell", () => {
366
368
  expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
367
369
  });
368
370
 
371
+ test("opens the layout browser from the primary Shift-L shortcut", async () => {
372
+ const config = createDefaultConfig("/tmp/gloomberb-shell-layout-browser-shortcut-test");
373
+ const opened: string[] = [];
374
+ const registry = createShellPluginRegistry();
375
+ registry.showPane = (paneId) => opened.push(paneId);
376
+ await renderShellForWindowModeTest(createInitialState(config), { registry });
377
+
378
+ await emitKeypress({ name: "l", ctrl: true, shift: true });
379
+
380
+ expect(opened).toEqual(["layout-marketplace"]);
381
+ });
382
+
369
383
  test("toggles the focused pane fullscreen without persisting layout", async () => {
370
384
  const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-shortcut-test");
371
385
  const mainPane = requireLayoutInstance(config, "portfolio-list:main");
@@ -478,8 +492,8 @@ describe("Shell", () => {
478
492
 
479
493
  const frame = testSetup!.captureCharFrame();
480
494
  const rows = frame.split("\n");
481
- expect(rows[2]?.indexOf("┌─:: Main Portfolio") ?? -1).toBeLessThan(0);
482
- expect(rows[5]?.indexOf("┌─:: Main Portfolio")).toBeGreaterThanOrEqual(14);
495
+ expect(rows[2]?.indexOf(":: Main Portfolio") ?? -1).toBeLessThan(0);
496
+ expect(rows[5]?.indexOf(":: Main Portfolio")).toBeGreaterThanOrEqual(14);
483
497
 
484
498
  await act(async () => {
485
499
  await testSetup!.mockMouse.release(16, 6);