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
@@ -32,6 +32,8 @@ export function menuForPane(
32
32
  openPaneSettings: (paneId: string) => void,
33
33
  desktopWindowBridge?: DesktopWindowBridge,
34
34
  copyPaneScreenshot?: (paneId: string) => void | Promise<void>,
35
+ sharePane?: () => void | Promise<void>,
36
+ linkItems: ContextMenuItem[] = [],
35
37
  ): ContextMenuItem[] {
36
38
  const baseActions: ContextMenuItem[] = [];
37
39
  if (pluginRegistry.hasPaneSettings(pane.instance.instanceId)) {
@@ -42,6 +44,14 @@ export function menuForPane(
42
44
  onSelect: () => openPaneSettings(pane.instance.instanceId),
43
45
  });
44
46
  }
47
+ if (sharePane) {
48
+ baseActions.push({
49
+ id: "share-pane",
50
+ label: "Share Pane",
51
+ accelerator: PANE_MANAGEMENT_ACCELERATORS.share,
52
+ onSelect: sharePane,
53
+ });
54
+ }
45
55
  if (copyPaneScreenshot) {
46
56
  baseActions.push({
47
57
  id: "copy-screenshot",
@@ -91,6 +101,10 @@ export function menuForPane(
91
101
  onSelect: () => persistLayout(removePane(layout, pane.instance.instanceId)),
92
102
  });
93
103
 
104
+ if (linkItems.length > 0) {
105
+ baseActions.push(contextMenuDivider("pane:link-divider"), ...linkItems);
106
+ }
107
+
94
108
  baseActions.push(
95
109
  contextMenuDivider("pane:layout-divider"),
96
110
  {
@@ -22,6 +22,7 @@ interface UseShellNativePointerRuntimeOptions {
22
22
  setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
23
23
  transientFocusActive: boolean;
24
24
  windowMode: WindowEditState | null;
25
+ commandBarOpen?: boolean;
25
26
  }
26
27
 
27
28
  export function useShellNativePointerRuntime({
@@ -38,6 +39,7 @@ export function useShellNativePointerRuntime({
38
39
  setMenuState,
39
40
  transientFocusActive,
40
41
  windowMode,
42
+ commandBarOpen = false,
41
43
  }: UseShellNativePointerRuntimeOptions) {
42
44
  const {
43
45
  dragRef,
@@ -59,6 +61,11 @@ export function useShellNativePointerRuntime({
59
61
  }, [focusPane, menuState, setHoveredMenuItemId, setMenuState]);
60
62
 
61
63
  const handleNativePaneMouseDown = useCallback((paneId: string, event: ShellMouseEvent) => {
64
+ if (commandBarOpen) {
65
+ event.stopPropagation();
66
+ event.preventDefault();
67
+ return;
68
+ }
62
69
  if (windowMode) {
63
70
  selectWindowModePane(paneId);
64
71
  event.stopPropagation();
@@ -69,10 +76,11 @@ export function useShellNativePointerRuntime({
69
76
  if (event.isDefaultPrevented?.()) return;
70
77
  focusNativePane(paneId);
71
78
  });
72
- }, [focusNativePane, selectWindowModePane, windowMode]);
79
+ }, [commandBarOpen, focusNativePane, selectWindowModePane, windowMode]);
73
80
 
74
81
  const startNativeFloatingDrag = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
75
82
  if (!nativePaneChrome) return;
83
+ if (commandBarOpen) return;
76
84
  if (windowMode) return;
77
85
  if (transientFocusActive) return;
78
86
  if (event.button === 2) return;
@@ -89,10 +97,11 @@ export function useShellNativePointerRuntime({
89
97
  };
90
98
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
91
99
  event.preventDefault();
92
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, updateDragFloatingRect, windowMode]);
100
+ }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, updateDragFloatingRect, windowMode]);
93
101
 
94
102
  const startNativeDockedDrag = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
95
103
  if (!nativePaneChrome) return;
104
+ if (commandBarOpen) return;
96
105
  if (windowMode) return;
97
106
  if (transientFocusActive) return;
98
107
  if (event.button === 2) return;
@@ -108,10 +117,11 @@ export function useShellNativePointerRuntime({
108
117
  origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
109
118
  };
110
119
  event.preventDefault();
111
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, windowMode]);
120
+ }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, windowMode]);
112
121
 
113
122
  const startNativeFloatResize = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
114
123
  if (!nativePaneChrome) return;
124
+ if (commandBarOpen) return;
115
125
  if (transientFocusActive) return;
116
126
 
117
127
  const pointer = getShellPointer(event);
@@ -130,10 +140,11 @@ export function useShellNativePointerRuntime({
130
140
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
131
141
  event.stopPropagation();
132
142
  event.preventDefault();
133
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, transientFocusActive, updateDragFloatingRect, windowMode]);
143
+ }, [commandBarOpen, dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, transientFocusActive, updateDragFloatingRect, windowMode]);
134
144
 
135
145
  const startNativeDividerDrag = useCallback((divider: DockDividerLayout, event: ShellMouseEvent) => {
136
146
  if (!nativePaneChrome) return;
147
+ if (commandBarOpen) return;
137
148
  if (transientFocusActive) return;
138
149
 
139
150
  const pointer = getShellPointer(event);
@@ -157,27 +168,30 @@ export function useShellNativePointerRuntime({
157
168
  });
158
169
  event.stopPropagation();
159
170
  event.preventDefault();
160
- }, [dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, transientFocusActive, updateDividerPreview]);
171
+ }, [commandBarOpen, dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, transientFocusActive, updateDividerPreview]);
161
172
 
162
173
  const handlePaneAction = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
174
+ if (commandBarOpen) return;
163
175
  if (windowMode) return;
164
176
  if (event.button === 2) return;
165
177
  event.stopPropagation();
166
178
  event.preventDefault();
167
179
  openPaneMenu(paneId, rect, event);
168
- }, [openPaneMenu, windowMode]);
180
+ }, [commandBarOpen, openPaneMenu, windowMode]);
169
181
 
170
182
  const handleNativePaneContextMenu = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
183
+ if (commandBarOpen) return;
171
184
  if (windowMode) return;
172
185
  openPaneMenu(paneId, rect, event);
173
- }, [openPaneMenu, windowMode]);
186
+ }, [commandBarOpen, openPaneMenu, windowMode]);
174
187
 
175
188
  const handleFloatingCloseMouseDown = useCallback((paneId: string, event: ShellMouseEvent) => {
189
+ if (commandBarOpen) return;
176
190
  if (windowMode) return;
177
191
  event.stopPropagation();
178
192
  event.preventDefault();
179
193
  handleFloatingClose(paneId);
180
- }, [handleFloatingClose, windowMode]);
194
+ }, [commandBarOpen, handleFloatingClose, windowMode]);
181
195
 
182
196
  return {
183
197
  handleFloatingCloseMouseDown,
@@ -54,10 +54,6 @@ export function useShellPaneActions({
54
54
  visibleLayout,
55
55
  width,
56
56
  }: UseShellPaneActionsOptions) {
57
- const openLayoutMenu = useCallback(() => {
58
- pluginRegistry.openCommandBar("LAY ");
59
- }, [pluginRegistry]);
60
-
61
57
  const openPaneSettings = useCallback((paneId: string) => {
62
58
  pluginRegistry.openPaneSettingsFn(paneId);
63
59
  closePaneMenu();
@@ -143,7 +139,6 @@ export function useShellPaneActions({
143
139
  gridlockVisiblePanes,
144
140
  handleFloatingClose,
145
141
  openFocusedPaneSettings,
146
- openLayoutMenu,
147
142
  openPaneSettings,
148
143
  popOutFocusedPane,
149
144
  toggleFocusedPaneFloating,
@@ -21,9 +21,10 @@ interface ShellPaneManagementShortcutOptions {
21
21
  hasActiveDrag(): boolean;
22
22
  inputCaptured: boolean;
23
23
  openFocusedPaneSettings(): boolean;
24
- openLayoutMenu(): void;
24
+ openLayoutGallery(): void;
25
25
  overlayOpen: boolean;
26
26
  popOutFocusedPane(): boolean;
27
+ shareFocusedPane(): boolean;
27
28
  startWindowMode(paneId?: string, mode?: WindowEditMode): void;
28
29
  toggleFocusedPaneFullscreen(): boolean;
29
30
  toggleFocusedPaneFloating(): boolean;
@@ -39,9 +40,10 @@ export function useShellPaneManagementShortcuts({
39
40
  hasActiveDrag,
40
41
  inputCaptured,
41
42
  openFocusedPaneSettings,
42
- openLayoutMenu,
43
+ openLayoutGallery,
43
44
  overlayOpen,
44
45
  popOutFocusedPane,
46
+ shareFocusedPane,
45
47
  startWindowMode,
46
48
  toggleFocusedPaneFullscreen,
47
49
  toggleFocusedPaneFloating,
@@ -119,8 +121,11 @@ export function useShellPaneManagementShortcuts({
119
121
  case "copy-screenshot":
120
122
  handled = copyFocusedPaneScreenshot();
121
123
  break;
122
- case "layout-actions":
123
- openLayoutMenu();
124
+ case "share":
125
+ handled = shareFocusedPane();
126
+ break;
127
+ case "layout-gallery":
128
+ openLayoutGallery();
124
129
  handled = true;
125
130
  break;
126
131
  case "gridlock-all":
@@ -6,9 +6,10 @@ export const PANE_MANAGEMENT_ACCELERATORS = {
6
6
  toggleFloating: "CmdOrCtrl+Shift+D",
7
7
  popOut: "CmdOrCtrl+Shift+O",
8
8
  copyScreenshot: "CmdOrCtrl+Shift+C",
9
+ share: "CmdOrCtrl+Shift+S",
9
10
  close: "CmdOrCtrl+W",
10
11
  closeAllFloating: "CmdOrCtrl+Alt+W",
11
- layoutActions: "CmdOrCtrl+Shift+L",
12
+ layoutGallery: "CmdOrCtrl+Shift+L",
12
13
  gridlockAll: "CmdOrCtrl+Shift+G",
13
14
  windowMode: "CmdOrCtrl+Shift+M",
14
15
  windowResizeMode: "CmdOrCtrl+Shift+R",
@@ -20,9 +21,10 @@ export type PaneManagementShortcut =
20
21
  | "toggle-floating"
21
22
  | "pop-out"
22
23
  | "copy-screenshot"
24
+ | "share"
23
25
  | "close"
24
26
  | "close-all-floating"
25
- | "layout-actions"
27
+ | "layout-gallery"
26
28
  | "gridlock-all"
27
29
  | "window-mode"
28
30
  | "window-resize-mode";
@@ -39,10 +41,11 @@ export function resolvePaneManagementShortcut(
39
41
  if (!shifted && name === ",") return "settings";
40
42
  if (!shifted || event.alt) return null;
41
43
  if (name === "c") return "copy-screenshot";
44
+ if (name === "s") return "share";
42
45
  if (name === "d") return "toggle-floating";
43
46
  if (name === "f") return "toggle-fullscreen";
44
47
  if (name === "o") return "pop-out";
45
- if (name === "l") return "layout-actions";
48
+ if (name === "l") return "layout-gallery";
46
49
  if (name === "g") return "gridlock-all";
47
50
  if (name === "m") return "window-mode";
48
51
  if (name === "r") return "window-resize-mode";
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createInitialState } from "../../../state/app/context";
3
+ import {
4
+ createDefaultConfig,
5
+ createPaneInstance,
6
+ findPaneInstance,
7
+ TICKER_RESEARCH_PANE_ID,
8
+ type LayoutConfig,
9
+ } from "../../../types/config";
10
+ import type { PaneDef } from "../../../types/plugin";
11
+ import { getPaneDisplayTitle } from "../pane/title";
12
+ import { tickerLinkMenuItems } from "./ticker-link-menu";
13
+
14
+ const panes = new Map<string, PaneDef>([
15
+ ["portfolio-list", {
16
+ id: "portfolio-list",
17
+ name: "Portfolio",
18
+ component: () => null,
19
+ defaultPosition: "left",
20
+ tickerSource: true,
21
+ }],
22
+ [TICKER_RESEARCH_PANE_ID, {
23
+ id: TICKER_RESEARCH_PANE_ID,
24
+ name: "Ticker Research",
25
+ component: () => null,
26
+ defaultPosition: "right",
27
+ }],
28
+ ]);
29
+
30
+ describe("tickerLinkMenuItems", () => {
31
+ test("pins the current ticker when unlinking and can link back to the source", () => {
32
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-link-menu-test"));
33
+ state.paneState["portfolio-list:main"] = { collectionId: "main", cursorSymbol: "AAPL" };
34
+ const target = findPaneInstance(state.config.layout, "ticker-detail:main")!;
35
+ let layout: LayoutConfig = state.config.layout;
36
+
37
+ expect(getPaneDisplayTitle(state, target, panes.get(TICKER_RESEARCH_PANE_ID)!, panes)).toBe(
38
+ "AAPL ⧉ Linked to Main Portfolio",
39
+ );
40
+
41
+ tickerLinkMenuItems({
42
+ instance: target,
43
+ layout,
44
+ panes,
45
+ state,
46
+ persistLayout: (nextLayout) => { layout = nextLayout; },
47
+ }).find((item) => item.id === "unlink:portfolio-list:main")?.onSelect?.();
48
+
49
+ const pinned = findPaneInstance(layout, target.instanceId)!;
50
+ expect(pinned.binding).toEqual({ kind: "fixed", symbol: "AAPL" });
51
+
52
+ const previousFollower = createPaneInstance(TICKER_RESEARCH_PANE_ID, {
53
+ instanceId: "ticker-detail:previous",
54
+ binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
55
+ });
56
+ layout = {
57
+ ...layout,
58
+ instances: [...layout.instances, previousFollower],
59
+ floating: [
60
+ ...layout.floating,
61
+ { instanceId: previousFollower.instanceId, x: 1, y: 1, width: 40, height: 12 },
62
+ ],
63
+ };
64
+ const pinnedState = { ...state, config: { ...state.config, layout } };
65
+ tickerLinkMenuItems({
66
+ instance: pinned,
67
+ layout,
68
+ panes,
69
+ state: pinnedState,
70
+ persistLayout: (nextLayout) => { layout = nextLayout; },
71
+ }).find((item) => item.id === "link:portfolio-list:main")?.onSelect?.();
72
+
73
+ expect(findPaneInstance(layout, target.instanceId)?.binding).toEqual({
74
+ kind: "follow",
75
+ sourceInstanceId: "portfolio-list:main",
76
+ });
77
+ expect(findPaneInstance(layout, previousFollower.instanceId)?.binding).toEqual({
78
+ kind: "fixed",
79
+ symbol: "AAPL",
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,78 @@
1
+ import { updatePaneInstance } from "../../../pane-settings";
2
+ import { listVisibleTickerSourcePanes } from "../../../plugins/ticker-navigation";
3
+ import { resolveTickerForPane, type AppState } from "../../../state/app/context";
4
+ import {
5
+ TICKER_RESEARCH_PANE_ID,
6
+ type LayoutConfig,
7
+ type PaneInstanceConfig,
8
+ } from "../../../types/config";
9
+ import type { ContextMenuItem } from "../../../types/context-menu";
10
+ import type { PaneDef } from "../../../types/plugin";
11
+ import { getPaneDisplayTitle } from "../pane/title";
12
+
13
+ /**
14
+ * Flat link controls for a Ticker Research pane: one entry per visible ticker source, plus an
15
+ * unlink entry that pins the pane on the symbol it currently shows.
16
+ */
17
+ export function tickerLinkMenuItems({
18
+ instance,
19
+ layout,
20
+ panes,
21
+ state,
22
+ persistLayout,
23
+ }: {
24
+ instance: PaneInstanceConfig;
25
+ layout: LayoutConfig;
26
+ panes: ReadonlyMap<string, PaneDef>;
27
+ state: Pick<AppState, "config" | "paneState">;
28
+ persistLayout: (nextLayout: LayoutConfig) => void;
29
+ }): ContextMenuItem[] {
30
+ if (instance.paneId !== TICKER_RESEARCH_PANE_ID) return [];
31
+
32
+ const sourceInstanceId = instance.binding?.kind === "follow" ? instance.binding.sourceInstanceId : null;
33
+ const rebind = (binding: PaneInstanceConfig["binding"]) => {
34
+ persistLayout(updatePaneInstance(layout, instance.instanceId, (current) => ({ ...current, binding })));
35
+ };
36
+ const linkToSource = (nextSourceId: string) => {
37
+ persistLayout({
38
+ ...layout,
39
+ instances: layout.instances.map((current) => {
40
+ if (current.instanceId === instance.instanceId) {
41
+ return { ...current, binding: { kind: "follow" as const, sourceInstanceId: nextSourceId } };
42
+ }
43
+ if (
44
+ current.paneId !== TICKER_RESEARCH_PANE_ID
45
+ || current.binding?.kind !== "follow"
46
+ || current.binding.sourceInstanceId !== nextSourceId
47
+ ) return current;
48
+
49
+ const symbol = resolveTickerForPane(state as AppState, current.instanceId);
50
+ return { ...current, binding: symbol ? { kind: "fixed" as const, symbol } : { kind: "none" as const } };
51
+ }),
52
+ });
53
+ };
54
+
55
+ return listVisibleTickerSourcePanes(layout, panes).flatMap((source): ContextMenuItem[] => {
56
+ const paneDef = panes.get(source.paneId);
57
+ if (!paneDef) return [];
58
+ const title = getPaneDisplayTitle(state, source, paneDef, panes);
59
+
60
+ if (source.instanceId !== sourceInstanceId) {
61
+ return [{
62
+ id: `link:${source.instanceId}`,
63
+ label: `Link to ${title}`,
64
+ onSelect: () => linkToSource(source.instanceId),
65
+ }];
66
+ }
67
+
68
+ // Unlinking keeps the pane alive, so it only works once a symbol has resolved.
69
+ const symbol = resolveTickerForPane(state as AppState, instance.instanceId);
70
+ return symbol
71
+ ? [{
72
+ id: `unlink:${source.instanceId}`,
73
+ label: `Unlink from ${title}`,
74
+ onSelect: () => rebind({ kind: "fixed", symbol }),
75
+ }]
76
+ : [];
77
+ });
78
+ }
@@ -1,9 +1,10 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { testRender } from "../../renderers/opentui/test-utils";
3
3
  import { AppContext, createInitialState } from "../../state/app/context";
4
- import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../types/config";
4
+ import { cloneLayout, createDefaultConfig, createPaneInstance, type LayoutConfig } from "../../types/config";
5
5
  import type { AppNotificationRequest } from "../../types/plugin";
6
6
  import { StatusBar } from "./status-bar";
7
+ import { getDockedPaneIds } from "../../plugins/pane-manager";
7
8
  import { setSharedRegistryForTests } from "../../plugins/registry";
8
9
  import { act, useEffect, useState } from "react";
9
10
  import { TransientLayoutProvider, useTransientLayout } from "./transient-layout";
@@ -185,34 +186,35 @@ describe("StatusBar", () => {
185
186
  expect(actions).toContainEqual({ type: "REORDER_LAYOUT", fromIndex: 0, toIndex: 2 });
186
187
  });
187
188
 
188
- test("shows a gridlock tip after a corner snap and runs gridlock on click", async () => {
189
- const config = createDefaultConfig("/tmp/gloomberb-test");
190
- const floatingLayout = cloneLayout(config.layout);
191
- floatingLayout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
192
- floatingLayout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 36, height: 12 }];
193
-
189
+ test("offers to tidy three floating windows and tiles them on click", async () => {
190
+ const config = createDefaultConfig("/tmp/gloomberb-tidy-test");
191
+ const floatingLayout: LayoutConfig = {
192
+ dockRoot: null,
193
+ instances: Array.from({ length: 3 }, (_, index) => createPaneInstance("chat", { instanceId: `chat-${index}` })),
194
+ floating: Array.from({ length: 3 }, (_, index) => ({
195
+ instanceId: `chat-${index}`,
196
+ x: index * 40,
197
+ y: 0,
198
+ width: 40,
199
+ height: 20,
200
+ zIndex: 50 + index,
201
+ })),
202
+ detached: [],
203
+ };
194
204
  const state = {
195
205
  ...createInitialState({
196
206
  ...config,
197
207
  layout: floatingLayout,
198
- layouts: [
199
- { name: "Default", layout: cloneLayout(floatingLayout) },
200
- { name: "Research", layout: cloneLayout(floatingLayout) },
201
- ],
208
+ layouts: [{ name: "Default", layout: cloneLayout(floatingLayout) }],
202
209
  }),
203
210
  statusBarVisible: true,
204
- gridlockTipVisible: true,
205
211
  };
206
-
207
212
  const actions: Array<{ type: string }> = [];
208
- let updatedLayout = null as ReturnType<typeof cloneLayout> | null;
213
+ let updatedLayout: LayoutConfig | null = null;
209
214
  const notifications: AppNotificationRequest[] = [];
210
215
 
211
216
  setSharedRegistryForTests({
212
- panes: new Map([
213
- ["portfolio-list", {}],
214
- [TICKER_RESEARCH_PANE_ID, {}],
215
- ]),
217
+ panes: new Map([["chat", { name: "Chat" }]]),
216
218
  getLayoutFn: () => state.config.layout,
217
219
  getTermSizeFn: () => ({ width: 120, height: 40 }),
218
220
  updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
@@ -228,76 +230,75 @@ describe("StatusBar", () => {
228
230
  );
229
231
 
230
232
  await testSetup.renderOnce();
231
-
232
233
  const frame = testSetup.captureCharFrame();
233
- expect(frame).toContain("Snapped a window?");
234
- expect(frame).toContain("Gridlock All");
235
-
236
- const buttonX = frame.split("\n")[0]?.indexOf("Gridlock All") ?? -1;
234
+ const buttonX = frame.split("\n")[0]?.indexOf("Tidy Windows") ?? -1;
237
235
  expect(buttonX).toBeGreaterThanOrEqual(0);
238
236
 
239
237
  await testSetup.mockMouse.click(buttonX + 1, 0);
240
238
  await testSetup.renderOnce();
241
239
 
242
240
  expect(updatedLayout?.floating).toHaveLength(0);
243
- expect(notifications).toHaveLength(1);
244
241
  expect(notifications[0]).toMatchObject({
245
- body: "Retiled all panes",
242
+ body: "Windows tidied",
246
243
  type: "success",
247
244
  action: { label: "Revert" },
248
245
  });
249
- expect(actions).toContainEqual({ type: "DISMISS_GRIDLOCK_TIP" });
250
-
251
246
  notifications[0]!.action!.onClick();
252
247
  expect(actions).toContainEqual({ type: "UNDO_LAYOUT" });
253
248
  });
254
249
 
255
- test("auto-dismisses the gridlock tip after its timeout", async () => {
256
- const config = createDefaultConfig("/tmp/gloomberb-test");
250
+ test("tidies covered windows instead of leaving them floating", async () => {
251
+ const config = createDefaultConfig("/tmp/gloomberb-covered-test");
252
+ const floatingLayout: LayoutConfig = {
253
+ dockRoot: null,
254
+ instances: Array.from({ length: 3 }, (_, index) => createPaneInstance("chat", { instanceId: `chat-${index}` })),
255
+ floating: Array.from({ length: 3 }, (_, index) => ({
256
+ instanceId: `chat-${index}`,
257
+ x: 10,
258
+ y: 4,
259
+ width: 50,
260
+ height: 20,
261
+ zIndex: 50 + index,
262
+ })),
263
+ detached: [],
264
+ };
257
265
  const state = {
258
- ...createInitialState(config),
266
+ ...createInitialState({
267
+ ...config,
268
+ layout: floatingLayout,
269
+ layouts: [{ name: "Default", layout: cloneLayout(floatingLayout) }],
270
+ }),
259
271
  statusBarVisible: true,
260
- gridlockTipVisible: true,
261
- gridlockTipSequence: 1,
262
272
  };
263
- const actions: Array<{ type: string }> = [];
264
- const timers: Array<{ callback: (() => void) | null; delay: number | undefined }> = [];
265
- const originalSetTimeout = globalThis.setTimeout;
266
- const originalClearTimeout = globalThis.clearTimeout;
267
-
268
- globalThis.setTimeout = ((callback: Parameters<typeof setTimeout>[0], delay?: number) => {
269
- timers.push({ callback: typeof callback === "function" ? callback : null, delay });
270
- return 1 as unknown as ReturnType<typeof setTimeout>;
271
- }) as typeof setTimeout;
272
- globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
273
+ let updatedLayout: LayoutConfig | null = null;
274
+ const notifications: AppNotificationRequest[] = [];
273
275
 
274
276
  setSharedRegistryForTests({
275
- panes: new Map(),
277
+ panes: new Map([["chat", { name: "Chat" }]]),
276
278
  getLayoutFn: () => state.config.layout,
277
279
  getTermSizeFn: () => ({ width: 120, height: 40 }),
278
- updateLayoutFn: () => {},
279
- notify: () => {},
280
+ updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
281
+ notify: (notification: AppNotificationRequest) => { notifications.push(notification); },
280
282
  Slot: () => null,
281
283
  } as any);
282
284
 
283
- try {
284
- testSetup = await testRender(
285
- <AppContext value={{ state, dispatch: (action) => actions.push(action as { type: string }) }}>
286
- <StatusBar />
287
- </AppContext>,
288
- { width: 120, height: 1 },
289
- );
285
+ testSetup = await testRender(
286
+ <AppContext value={{ state, dispatch: () => {} }}>
287
+ <StatusBar />
288
+ </AppContext>,
289
+ { width: 120, height: 1 },
290
+ );
290
291
 
291
- await testSetup.renderOnce();
292
+ await testSetup.renderOnce();
293
+ const frame = testSetup.captureCharFrame();
294
+ const tidyX = frame.split("\n")[0]?.indexOf("Tidy Windows") ?? -1;
295
+ expect(tidyX).toBeGreaterThanOrEqual(0);
292
296
 
293
- const gridlockTimer = timers.find((entry) => entry.delay === 60_000);
294
- expect(gridlockTimer?.callback).toBeDefined();
295
- gridlockTimer?.callback?.();
297
+ await testSetup.mockMouse.click(tidyX + 1, 0);
298
+ await testSetup.renderOnce();
296
299
 
297
- expect(actions).toContainEqual({ type: "DISMISS_GRIDLOCK_TIP" });
298
- } finally {
299
- globalThis.setTimeout = originalSetTimeout;
300
- globalThis.clearTimeout = originalClearTimeout;
301
- }
300
+ expect(updatedLayout?.floating).toHaveLength(0);
301
+ expect(getDockedPaneIds(updatedLayout!)).toHaveLength(3);
302
+ expect(notifications[0]?.body).toBe("Windows tidied");
302
303
  });
303
304
  });