gloomberb 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +10 -9
  2. package/README.zh-CN.md +6 -6
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +7 -0
  5. package/src/api-client/index.test.ts +47 -0
  6. package/src/api-client/index.ts +61 -0
  7. package/src/api-client/paths.ts +16 -0
  8. package/src/api-client/types.ts +35 -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/cli/commands/automation.test.ts +9 -0
  19. package/src/cli/commands/automation.ts +22 -73
  20. package/src/cli/index.ts +0 -2
  21. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  22. package/src/components/chart/composite/composite-chart.tsx +12 -9
  23. package/src/components/chart/composite/scene.ts +0 -21
  24. package/src/components/chart/composite/tools.ts +49 -0
  25. package/src/components/command-bar/commands/direct/index.ts +4 -0
  26. package/src/components/command-bar/commands/registry.ts +8 -1
  27. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  28. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  29. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  30. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  31. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  32. package/src/components/command-bar/surface/index.test.tsx +6 -6
  33. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  34. package/src/components/command-bar/view-model.test.ts +2 -1
  35. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  36. package/src/components/command-bar/workflow/ops.ts +1 -1
  37. package/src/components/layout/detached-pane-shell.tsx +74 -10
  38. package/src/components/layout/pane/title.ts +31 -3
  39. package/src/components/layout/shell/active-drag.ts +4 -11
  40. package/src/components/layout/shell/drag/index.ts +4 -20
  41. package/src/components/layout/shell/drag/runtime.ts +3 -4
  42. package/src/components/layout/shell/index.test.tsx +17 -3
  43. package/src/components/layout/shell/index.tsx +86 -30
  44. package/src/components/layout/shell/menu.test.ts +37 -1
  45. package/src/components/layout/shell/menu.ts +14 -0
  46. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  47. package/src/components/layout/shell/pane/actions.ts +0 -5
  48. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  49. package/src/components/layout/shell/shortcuts.ts +6 -3
  50. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  51. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  52. package/src/components/layout/status-bar.test.tsx +62 -61
  53. package/src/components/layout/status-bar.tsx +53 -75
  54. package/src/core/state/app/layout-reducer.ts +36 -0
  55. package/src/core/state/app/layout.ts +10 -5
  56. package/src/core/state/app/state.ts +0 -13
  57. package/src/core/state/app/types.ts +6 -4
  58. package/src/data/config/chart-settings.ts +1 -1
  59. package/src/data/config/layout.ts +2 -0
  60. package/src/data/config/store/index.test.ts +27 -6
  61. package/src/data/config/store/normalize.ts +3 -5
  62. package/src/data/plugin-state-store.ts +11 -0
  63. package/src/data/sqlite/cache.test.ts +10 -0
  64. package/src/i18n/es.ts +3 -4
  65. package/src/i18n/ja.ts +3 -4
  66. package/src/i18n/ko.ts +3 -4
  67. package/src/i18n/zh-cn.ts +3 -4
  68. package/src/i18n/zh-tw.ts +3 -4
  69. package/src/layout-marketplace/api.test.ts +54 -0
  70. package/src/layout-marketplace/api.ts +65 -0
  71. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  72. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  73. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  74. package/src/layout-marketplace/gallery.test.tsx +84 -0
  75. package/src/layout-marketplace/gallery.tsx +323 -0
  76. package/src/layout-marketplace/geometry.test.ts +47 -0
  77. package/src/layout-marketplace/geometry.ts +80 -0
  78. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  79. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  80. package/src/layout-marketplace/model.test.ts +118 -0
  81. package/src/layout-marketplace/model.ts +171 -0
  82. package/src/layout-marketplace/name-dialog.tsx +60 -0
  83. package/src/layout-marketplace/pane-imagery.ts +85 -0
  84. package/src/layout-marketplace/pane.tsx +20 -0
  85. package/src/layout-marketplace/payload.test.ts +246 -0
  86. package/src/layout-marketplace/payload.ts +624 -0
  87. package/src/layout-marketplace/use-marketplace.ts +63 -0
  88. package/src/market-data/hooks.tsx +0 -41
  89. package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
  90. package/src/plugins/broker-sync/normalize.ts +9 -2
  91. package/src/plugins/builtin/ai/index.tsx +18 -2
  92. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  93. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  94. package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
  95. package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
  96. package/src/plugins/builtin/alerts/index.tsx +4 -0
  97. package/src/plugins/builtin/alerts/pane.tsx +2 -8
  98. package/src/plugins/builtin/analytics/index.tsx +3 -0
  99. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  100. package/src/plugins/builtin/cds/client.test.ts +83 -0
  101. package/src/plugins/builtin/cds/client.ts +67 -0
  102. package/src/plugins/builtin/cds/index.tsx +41 -0
  103. package/src/plugins/builtin/cds/model.test.ts +232 -0
  104. package/src/plugins/builtin/cds/model.ts +401 -0
  105. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  106. package/src/plugins/builtin/cds/pane.tsx +358 -0
  107. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  108. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  109. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  110. package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
  111. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  112. package/src/plugins/builtin/chat/controller/index.ts +8 -1
  113. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  114. package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
  115. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  116. package/src/plugins/builtin/chat/controller.test.ts +64 -14
  117. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  118. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  119. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  120. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  121. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  122. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  123. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  124. package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
  125. package/src/plugins/builtin/cloud/auth-model.ts +4 -1
  126. package/src/plugins/builtin/cloud/plugin.tsx +11 -1
  127. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  128. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  129. package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
  130. package/src/plugins/builtin/composite-plugins.ts +3 -1
  131. package/src/plugins/builtin/connections/index.ts +3 -0
  132. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  133. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  134. package/src/plugins/builtin/help/index.tsx +2 -2
  135. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  136. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  137. package/src/plugins/builtin/news/index.tsx +1 -0
  138. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  139. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  140. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  141. package/src/plugins/builtin/options/index.tsx +1 -0
  142. package/src/plugins/builtin/options/view.test.tsx +25 -3
  143. package/src/plugins/builtin/options/view.tsx +1 -0
  144. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
  145. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  146. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  147. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  148. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  149. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  150. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  151. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  152. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  153. package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
  154. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  155. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
  156. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
  157. package/src/plugins/builtin/research/index.tsx +4 -0
  158. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  159. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  160. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  161. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  162. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  163. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  164. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  165. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  166. package/src/plugins/catalog-browser.test.ts +10 -0
  167. package/src/plugins/catalog-browser.ts +3 -1
  168. package/src/plugins/gridlock-notification.ts +2 -1
  169. package/src/plugins/pane-manager/gridlock.ts +87 -3
  170. package/src/plugins/pane-manager.test.ts +30 -0
  171. package/src/plugins/pane-manager.ts +6 -1
  172. package/src/plugins/prediction-markets/chart.tsx +2 -2
  173. package/src/plugins/prediction-markets/controller/index.ts +5 -1
  174. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  175. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  176. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  177. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  178. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  179. package/src/plugins/prediction-markets/index.tsx +8 -4
  180. package/src/plugins/prediction-markets/navigation.ts +0 -1
  181. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  182. package/src/plugins/prediction-markets/pane.tsx +4 -0
  183. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  184. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  185. package/src/plugins/registry/index.ts +2 -0
  186. package/src/plugins/ticker-navigation.test.ts +24 -0
  187. package/src/plugins/ticker-navigation.ts +40 -0
  188. package/src/remote/schema.ts +1 -1
  189. package/src/renderers/browser/cloud-transport.test.ts +36 -0
  190. package/src/renderers/browser/cloud-transport.ts +19 -0
  191. package/src/renderers/browser/config-host.ts +0 -4
  192. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  193. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  194. package/src/renderers/browser/main.tsx +3 -0
  195. package/src/renderers/cloudflare/worker.test.ts +8 -5
  196. package/src/renderers/cloudflare/worker.ts +6 -5
  197. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  198. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  199. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  201. package/src/renderers/electrobun/view/host/native.ts +0 -5
  202. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  203. package/src/renderers/electrobun/view/styles.css +107 -7
  204. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  205. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  206. package/src/renderers/electrobun/view/ui-host.tsx +4 -1
  207. package/src/renderers/share/layout-view.test.tsx +58 -0
  208. package/src/renderers/share/layout-view.tsx +133 -0
  209. package/src/renderers/share/main.tsx +54 -6
  210. package/src/renderers/share/styles.css +35 -0
  211. package/src/renderers/share/view.test.tsx +78 -0
  212. package/src/renderers/share/view.tsx +71 -0
  213. package/src/shares/api.test.ts +52 -3
  214. package/src/shares/api.ts +39 -15
  215. package/src/shares/location.test.ts +19 -0
  216. package/src/shares/location.ts +11 -0
  217. package/src/shares/pane.test.ts +110 -0
  218. package/src/shares/pane.ts +54 -0
  219. package/src/shares/payload.ts +74 -1
  220. package/src/state/app/bootstrap.ts +6 -7
  221. package/src/state/app/context/index.test.ts +41 -1
  222. package/src/state/app/context/index.test.tsx +34 -16
  223. package/src/state/selectors-ui.ts +0 -8
  224. package/src/sync/controller.test.ts +57 -1
  225. package/src/sync/controller.ts +22 -11
  226. package/src/sync/react.ts +13 -0
  227. package/src/theme/font-scale.ts +0 -11
  228. package/src/types/config.ts +35 -5
  229. package/src/types/desktop-menu.ts +1 -0
  230. package/src/types/plugin.ts +36 -0
  231. package/src/ui/context-menu.tsx +1 -1
  232. package/src/ui/toast.tsx +2 -0
  233. package/src/utils/back-navigation.ts +0 -4
@@ -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);
@@ -21,6 +21,7 @@ import {
21
21
  syncConfigActiveLayoutState,
22
22
  useAppDispatch,
23
23
  useAppSelector,
24
+ useAppStateRef,
24
25
  } from "../../../state/app/context";
25
26
  import {
26
27
  selectCommandBarOpen,
@@ -37,6 +38,7 @@ import {
37
38
  menuForPane,
38
39
  menuItemsForFallback,
39
40
  } from "./menu";
41
+ import { tickerLinkMenuItems } from "./ticker-link-menu";
40
42
  import {
41
43
  makeSnapGuides,
42
44
  resolveExternalDockPreview,
@@ -67,6 +69,9 @@ import {
67
69
  resolveShellCursorOcclusionRects,
68
70
  useShellCursorOcclusionGuard,
69
71
  } from "./cursor-occlusion";
72
+ import { createShare, openLiveShareUrl } from "../../../shares/api";
73
+ import { buildPaneSharePayload } from "../../../shares/pane";
74
+ import type { SharePayload } from "../../../shares/payload";
70
75
 
71
76
  export { resolveAppHeaderHeightCells } from "./chrome";
72
77
  export { buildNativeWindowState } from "./native/window-state";
@@ -100,13 +105,14 @@ export function Shell({
100
105
  const previousFocusedPaneId = useAppSelector((state) => state.previousFocusedPaneId);
101
106
  const activePanel = useAppSelector((state) => state.activePanel);
102
107
  const commandBarOpen = useAppSelector(selectCommandBarOpen);
108
+ const stateRef = useAppStateRef();
103
109
  const inputCaptured = useAppSelector((state) => state.inputCaptured);
104
110
  const statusBarVisible = useAppSelector(selectStatusBarVisible);
105
111
  const rendererHost = useRendererHost();
106
112
  const { setTransientLayout } = useTransientLayout();
107
113
  const uiKind = useUiHost().kind;
108
114
  const shortcutDisplayMode = getShortcutDisplayMode(uiKind);
109
- const { nativePaneChrome = false, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
115
+ const { nativePaneChrome = false, nativeContextMenu, precisePointer, publicSharing, titleBarOverlay, cellHeightPx } = useUiCapabilities();
110
116
  const { showContextMenu } = useContextMenu();
111
117
  const { width, height } = useViewport();
112
118
  const shellRef = useRef<BoxRenderable | null>(null);
@@ -119,8 +125,9 @@ export function Shell({
119
125
  const dialogOpen = useDialogState((dialog) => dialog.isOpen);
120
126
  const [hoveredPaneId, setHoveredPaneId] = useState<string | null>(null);
121
127
  const setHoveredPaneIfChanged = useCallback((paneId: string | null) => {
128
+ if (commandBarOpen) return;
122
129
  setHoveredPaneId((current) => (current === paneId ? current : paneId));
123
- }, []);
130
+ }, [commandBarOpen]);
124
131
  const [menuState, setMenuState] = useState<ActionMenuState | null>(null);
125
132
  const [transientFocusLayoutState, setTransientFocusLayoutState] = useState<TransientFocusLayoutState | null>(null);
126
133
  const transientFocusLayoutStateRef = useRef<TransientFocusLayoutState | null>(null);
@@ -131,6 +138,9 @@ export function Shell({
131
138
  setHoveredMenuItemId(null);
132
139
  }, []);
133
140
  const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
141
+ useEffect(() => {
142
+ if (commandBarOpen) setHoveredPaneId(null);
143
+ }, [commandBarOpen]);
134
144
 
135
145
  const dragRuntime = useShellDragRuntimeState({
136
146
  contentHeight,
@@ -165,13 +175,14 @@ export function Shell({
165
175
  dispatch(hasFocusTarget
166
176
  ? { type: "UPDATE_LAYOUT", layout: nextLayout, focusedPaneId: options.focusedPaneId ?? null }
167
177
  : { type: "UPDATE_LAYOUT", layout: nextLayout });
178
+ const currentState = stateRef.current;
168
179
  scheduleConfigSave(syncConfigActiveLayoutState(
169
- { ...config, layout: nextLayout },
170
- paneState,
171
- hasFocusTarget ? (options.focusedPaneId ?? null) : focusedPaneId,
172
- activePanel,
180
+ { ...currentState.config, layout: nextLayout },
181
+ currentState.paneState,
182
+ hasFocusTarget ? (options.focusedPaneId ?? null) : currentState.focusedPaneId,
183
+ currentState.activePanel,
173
184
  ));
174
- }, [activePanel, config, dispatch, focusedPaneId, paneState]);
185
+ }, [dispatch, stateRef]);
175
186
 
176
187
  const focusPane = useCallback((paneId: string) => {
177
188
  dispatch({ type: "FOCUS_PANE", paneId });
@@ -253,7 +264,6 @@ export function Shell({
253
264
  gridlockVisiblePanes,
254
265
  handleFloatingClose,
255
266
  openFocusedPaneSettings,
256
- openLayoutMenu,
257
267
  openPaneSettings,
258
268
  popOutFocusedPane,
259
269
  toggleFocusedPaneFloating,
@@ -272,6 +282,9 @@ export function Shell({
272
282
  visibleLayout,
273
283
  width,
274
284
  });
285
+ const openLayoutGallery = useCallback(() => {
286
+ pluginRegistry.showPane("layout-marketplace");
287
+ }, [pluginRegistry]);
275
288
  const setTransientFocusLayout = useCallback((next: TransientFocusLayoutState | null) => {
276
289
  transientFocusLayoutStateRef.current = next;
277
290
  setTransientFocusLayoutState(next);
@@ -369,24 +382,6 @@ export function Shell({
369
382
  transientFocusLayoutState,
370
383
  ]);
371
384
 
372
- useShellPaneManagementShortcuts({
373
- cancelActiveDrag,
374
- closeAllFloatingPanes,
375
- closeFocusedPane,
376
- copyFocusedPaneScreenshot,
377
- focusedPaneId,
378
- gridlockVisiblePanes,
379
- hasActiveDrag,
380
- inputCaptured,
381
- openFocusedPaneSettings,
382
- openLayoutMenu,
383
- overlayOpen,
384
- popOutFocusedPane,
385
- startWindowMode,
386
- toggleFocusedPaneFullscreen,
387
- toggleFocusedPaneFloating,
388
- });
389
-
390
385
  const dockLeafLayouts = useMemo(() => getDockLeafLayouts(activeLayout, bounds, dockGeometryOptions), [activeLayout, bounds, dockGeometryOptions]);
391
386
  const dockDividerLayouts = useMemo(() => getDockDividerLayouts(activeLayout, bounds, dockGeometryOptions), [activeLayout, bounds, dockGeometryOptions]);
392
387
  const snapGuides = useMemo(() => makeSnapGuides(width, contentHeight), [contentHeight, width]);
@@ -423,8 +418,8 @@ export function Shell({
423
418
  [config, paneState],
424
419
  );
425
420
  const getPaneTitle = useCallback(
426
- (pane: ResolvedPane): string => getPaneDisplayTitle(titleState, pane.instance, pane.def),
427
- [titleState],
421
+ (pane: ResolvedPane): string => getPaneDisplayTitle(titleState, pane.instance, pane.def, pluginRegistry.panes),
422
+ [pluginRegistry.panes, titleState],
428
423
  );
429
424
  const handlePaneQuickSetting = useCallback((paneId: string, key: string, event: any) => {
430
425
  event?.preventDefault?.();
@@ -447,6 +442,51 @@ export function Shell({
447
442
  onMouseDown: (event) => handlePaneQuickSetting(paneId, setting.key, event),
448
443
  }))
449
444
  ), [config, handlePaneQuickSetting, pluginRegistry]);
445
+ const sharePane = useCallback(async (payload: Extract<SharePayload, { kind: "pane" }>) => {
446
+ try {
447
+ const { id } = await createShare(payload);
448
+ await rendererHost.copyText(openLiveShareUrl(id));
449
+ pluginRegistry.notify({ body: "Share link copied to clipboard", type: "success" });
450
+ } catch (error) {
451
+ pluginRegistry.notify({
452
+ body: error instanceof Error ? error.message : "Could not share this pane.",
453
+ type: "error",
454
+ });
455
+ }
456
+ }, [pluginRegistry, rendererHost]);
457
+ const shareFocusedPane = useCallback(() => {
458
+ if (!publicSharing || !focusedPaneId) return false;
459
+ const pane = paneMap.get(focusedPaneId);
460
+ if (!pane) return false;
461
+ const payload = buildPaneSharePayload(
462
+ pluginRegistry,
463
+ pane.instance,
464
+ paneState[focusedPaneId] ?? {},
465
+ resolveTickerForPane(titleState, focusedPaneId),
466
+ );
467
+ if (!payload) return false;
468
+ void sharePane(payload);
469
+ return true;
470
+ }, [focusedPaneId, paneMap, paneState, pluginRegistry, publicSharing, sharePane, titleState]);
471
+
472
+ useShellPaneManagementShortcuts({
473
+ cancelActiveDrag,
474
+ closeAllFloatingPanes,
475
+ closeFocusedPane,
476
+ copyFocusedPaneScreenshot,
477
+ focusedPaneId,
478
+ gridlockVisiblePanes,
479
+ hasActiveDrag,
480
+ inputCaptured,
481
+ openFocusedPaneSettings,
482
+ openLayoutGallery,
483
+ overlayOpen,
484
+ popOutFocusedPane,
485
+ shareFocusedPane,
486
+ startWindowMode,
487
+ toggleFocusedPaneFullscreen,
488
+ toggleFocusedPaneFloating,
489
+ });
450
490
 
451
491
  const openPaneMenu = useCallback((paneId: string, rect: LayoutBounds, event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
452
492
  const pane = paneMap.get(paneId);
@@ -459,6 +499,14 @@ export function Shell({
459
499
  title: getPaneTitle(pane),
460
500
  floating: !!pane.floating,
461
501
  };
502
+ const sharePayload = publicSharing
503
+ ? buildPaneSharePayload(
504
+ pluginRegistry,
505
+ pane.instance,
506
+ paneState[paneId] ?? {},
507
+ resolveTickerForPane(titleState, paneId),
508
+ )
509
+ : null;
462
510
  const items = menuForPane(
463
511
  pane,
464
512
  visibleLayout,
@@ -470,6 +518,14 @@ export function Shell({
470
518
  openPaneSettings,
471
519
  desktopWindowBridge,
472
520
  nativePaneChrome && rendererHost.copyPngImage ? copyPaneScreenshot : undefined,
521
+ sharePayload ? () => sharePane(sharePayload) : undefined,
522
+ tickerLinkMenuItems({
523
+ instance: pane.instance,
524
+ layout: visibleLayout,
525
+ panes: pluginRegistry.panes,
526
+ state: titleState,
527
+ persistLayout,
528
+ }),
473
529
  );
474
530
  void showContextMenu(context, items, event).then((shown) => {
475
531
  if (shown) return;
@@ -493,7 +549,7 @@ export function Shell({
493
549
  items: fallbackItems,
494
550
  });
495
551
  });
496
- }, [contentHeight, copyPaneScreenshot, desktopWindowBridge, focusPane, getPaneTitle, nativePaneChrome, openPaneSettings, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, shortcutDisplayMode, showContextMenu, visibleLayout, width]);
552
+ }, [contentHeight, copyPaneScreenshot, desktopWindowBridge, focusPane, getPaneTitle, nativePaneChrome, openPaneSettings, paneMap, paneState, persistLayout, pluginRegistry, publicSharing, rendererHost.copyPngImage, sharePane, shortcutDisplayMode, showContextMenu, titleState, visibleLayout, width]);
497
553
 
498
554
  const {
499
555
  handleFloatingCloseMouseDown,
@@ -511,7 +567,6 @@ export function Shell({
511
567
  bounds,
512
568
  closePaneMenu,
513
569
  contentHeight,
514
- dispatch,
515
570
  dockGeometryOptions,
516
571
  dockDividerLayouts,
517
572
  dockLeafLayouts,
@@ -535,6 +590,7 @@ export function Shell({
535
590
  visibleLayout,
536
591
  width,
537
592
  windowMode,
593
+ commandBarOpen,
538
594
  });
539
595
  const windowModeDockResizePathKey = windowMode?.focus.kind === "dock-resize"
540
596
  ? windowMode.focus.pathKey
@@ -1,7 +1,43 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { displayWidth } from "../../../utils/format";
3
3
  import { getLanguage, setLanguage, t } from "../../../i18n";
4
- import { actionMenuWidth } from "./menu";
4
+ import { actionMenuWidth, menuForPane } from "./menu";
5
+
6
+ describe("pane action menu", () => {
7
+ test("offers the global share action for a portable pane", async () => {
8
+ let shared = false;
9
+ const items = menuForPane(
10
+ {
11
+ instance: { instanceId: "chart-1", paneId: "chart-composer" },
12
+ def: { defaultPosition: "right" },
13
+ floating: false,
14
+ } as any,
15
+ {
16
+ dockRoot: { kind: "pane", instanceId: "chart-1" },
17
+ instances: [{ instanceId: "chart-1", paneId: "chart-composer" }],
18
+ floating: [],
19
+ detached: [],
20
+ },
21
+ 120,
22
+ 40,
23
+ {
24
+ hasPaneSettings: () => false,
25
+ openWindowMode: () => {},
26
+ } as any,
27
+ () => {},
28
+ () => {},
29
+ () => {},
30
+ undefined,
31
+ undefined,
32
+ () => { shared = true; },
33
+ );
34
+
35
+ const share = items.find((item) => item.type !== "divider" && item.id === "share-pane");
36
+ await share?.onSelect?.();
37
+ expect(shared).toBe(true);
38
+ expect(share?.accelerator).toBe("CmdOrCtrl+Shift+S");
39
+ });
40
+ });
5
41
 
6
42
  describe("action menu sizing", () => {
7
43
  test("uses translated terminal display width", () => {