gloomberb 0.9.1 → 0.9.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 (136) hide show
  1. package/README.md +23 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app.tsx +10 -7
  5. package/src/capabilities/registry.ts +1 -0
  6. package/src/capabilities/types.ts +23 -0
  7. package/src/cli/commands/api.ts +119 -0
  8. package/src/cli/commands/automation.ts +173 -0
  9. package/src/cli/commands/command-utils.ts +53 -0
  10. package/src/cli/commands/market.ts +527 -0
  11. package/src/cli/commands/overview.ts +222 -0
  12. package/src/cli/commands/search.ts +39 -0
  13. package/src/cli/commands/system.ts +553 -0
  14. package/src/cli/commands/ticker.ts +104 -12
  15. package/src/cli/context.ts +35 -2
  16. package/src/cli/entry.ts +51 -0
  17. package/src/cli/errors.ts +56 -5
  18. package/src/cli/index.ts +73 -11
  19. package/src/cli/options.test.ts +38 -0
  20. package/src/cli/options.ts +95 -0
  21. package/src/cli/pane-functions/index.ts +10 -1
  22. package/src/cli/registry.test.ts +40 -0
  23. package/src/cli/registry.ts +46 -5
  24. package/src/cli/result.test.ts +82 -0
  25. package/src/cli/result.ts +149 -0
  26. package/src/cli/types.ts +11 -0
  27. package/src/cli.test.ts +11 -11
  28. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
  29. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  30. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
  31. package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
  32. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  33. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  34. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  35. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  36. package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
  37. package/src/components/chart/core/pane-settings.test.ts +27 -0
  38. package/src/components/chart/core/pane-settings.ts +60 -1
  39. package/src/components/chart/core/pointer.ts +2 -0
  40. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  41. package/src/components/chart/stock-chart/settings.ts +7 -3
  42. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  43. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  44. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  45. package/src/components/data-table/view.tsx +13 -2
  46. package/src/components/feed-data-table/stack-view.tsx +9 -1
  47. package/src/components/index.ts +1 -1
  48. package/src/components/input-search-bar.test.tsx +103 -0
  49. package/src/components/input-search-bar.tsx +16 -0
  50. package/src/components/layout/detached-pane-shell.tsx +5 -2
  51. package/src/components/layout/header.tsx +13 -5
  52. package/src/components/layout/shell/active-drag.ts +5 -1
  53. package/src/components/layout/shell/drag/runtime.ts +21 -3
  54. package/src/components/layout/shell/fullscreen.ts +10 -0
  55. package/src/components/layout/shell/index.test.tsx +395 -2
  56. package/src/components/layout/shell/index.tsx +128 -2
  57. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  58. package/src/components/layout/shell/pane/layers.tsx +26 -14
  59. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  60. package/src/components/layout/shell/shortcuts.ts +4 -0
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  62. package/src/components/layout/shell/window-mode/index.ts +11 -3
  63. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  64. package/src/components/layout/status-bar.test.tsx +105 -0
  65. package/src/components/layout/status-bar.tsx +32 -6
  66. package/src/components/layout/transient-layout.tsx +57 -0
  67. package/src/components/layout/window-controls.tsx +19 -10
  68. package/src/components/layout/window-edit/mode.test.ts +76 -5
  69. package/src/components/layout/window-edit/mode.ts +47 -2
  70. package/src/components/layout/window-edit/presentation.ts +2 -1
  71. package/src/components/price-performance.tsx +183 -0
  72. package/src/core/app-services.ts +7 -4
  73. package/src/data/config/store/index.ts +1 -2
  74. package/src/market-data/performance.test.ts +54 -0
  75. package/src/market-data/performance.ts +109 -0
  76. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  77. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  78. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  79. package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
  80. package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
  81. package/src/plugins/builtin/congress-trades/footer.ts +14 -8
  82. package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
  83. package/src/plugins/builtin/congress-trades/model.ts +29 -0
  84. package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
  85. package/src/plugins/builtin/congress-trades/table.tsx +10 -1
  86. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  87. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  88. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  89. package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
  90. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  91. package/src/plugins/builtin/shared/read-state.ts +43 -0
  92. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  93. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  94. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  95. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  96. package/src/plugins/builtin/substack/pane.tsx +8 -3
  97. package/src/plugins/builtin/substack/read-state.ts +55 -0
  98. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  99. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  100. package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
  102. package/src/plugins/pane-manager/docking.ts +9 -2
  103. package/src/plugins/pane-manager/queries.ts +8 -2
  104. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  105. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  106. package/src/plugins/prediction-markets/pane.test.tsx +32 -0
  107. package/src/plugins/prediction-markets/pane.tsx +21 -1
  108. package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
  109. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  110. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  111. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  112. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  113. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
  114. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  115. package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
  116. package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
  117. package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
  118. package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
  119. package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
  120. package/src/renderers/electrobun/bun/index.ts +22 -2
  121. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  122. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  123. package/src/renderers/electrobun/shared/protocol.ts +1 -0
  124. package/src/renderers/electrobun/view/app-services.ts +6 -4
  125. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  126. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  127. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  128. package/src/renderers/electrobun/view/main.tsx +2 -1
  129. package/src/renderers/electrobun/view/styles.css +9 -8
  130. package/src/renderers/electrobun/view/ui-host.tsx +102 -77
  131. package/src/renderers/opentui/start.tsx +13 -5
  132. package/src/state/app/input-capture.ts +12 -0
  133. package/src/types/plugin.ts +28 -0
  134. package/src/utils/cli-output.ts +6 -0
  135. package/src/utils/search-focus-navigation.ts +24 -0
  136. package/src/version.ts +1 -1
@@ -20,6 +20,7 @@ interface UseShellNativePointerRuntimeOptions {
20
20
  selectWindowModePane: (paneId: string) => void;
21
21
  setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
22
22
  setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
23
+ transientFocusActive: boolean;
23
24
  windowMode: WindowEditState | null;
24
25
  }
25
26
 
@@ -35,6 +36,7 @@ export function useShellNativePointerRuntime({
35
36
  selectWindowModePane,
36
37
  setHoveredMenuItemId,
37
38
  setMenuState,
39
+ transientFocusActive,
38
40
  windowMode,
39
41
  }: UseShellNativePointerRuntimeOptions) {
40
42
  const {
@@ -69,6 +71,7 @@ export function useShellNativePointerRuntime({
69
71
  const startNativeFloatingDrag = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
70
72
  if (!nativePaneChrome) return;
71
73
  if (windowMode) return;
74
+ if (transientFocusActive) return;
72
75
  if (event.button === 2) return;
73
76
 
74
77
  const pointer = getShellPointer(event);
@@ -83,11 +86,12 @@ export function useShellNativePointerRuntime({
83
86
  };
84
87
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
85
88
  event.preventDefault();
86
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, updateDragFloatingRect, windowMode]);
89
+ }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, updateDragFloatingRect, windowMode]);
87
90
 
88
91
  const startNativeDockedDrag = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
89
92
  if (!nativePaneChrome) return;
90
93
  if (windowMode) return;
94
+ if (transientFocusActive) return;
91
95
  if (event.button === 2) return;
92
96
 
93
97
  const pointer = getShellPointer(event);
@@ -101,10 +105,11 @@ export function useShellNativePointerRuntime({
101
105
  origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
102
106
  };
103
107
  event.preventDefault();
104
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, windowMode]);
108
+ }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, transientFocusActive, windowMode]);
105
109
 
106
110
  const startNativeFloatResize = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
107
111
  if (!nativePaneChrome) return;
112
+ if (transientFocusActive) return;
108
113
 
109
114
  const pointer = getShellPointer(event);
110
115
  if (windowMode) {
@@ -122,10 +127,11 @@ export function useShellNativePointerRuntime({
122
127
  updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
123
128
  event.stopPropagation();
124
129
  event.preventDefault();
125
- }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, updateDragFloatingRect, windowMode]);
130
+ }, [dragRef, focusNativePane, getShellPointer, nativePaneChrome, selectWindowModePane, transientFocusActive, updateDragFloatingRect, windowMode]);
126
131
 
127
132
  const startNativeDividerDrag = useCallback((divider: DockDividerLayout, event: ShellMouseEvent) => {
128
133
  if (!nativePaneChrome) return;
134
+ if (transientFocusActive) return;
129
135
 
130
136
  const pointer = getShellPointer(event);
131
137
  if (menuState) {
@@ -148,7 +154,7 @@ export function useShellNativePointerRuntime({
148
154
  });
149
155
  event.stopPropagation();
150
156
  event.preventDefault();
151
- }, [dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, updateDividerPreview]);
157
+ }, [dragRef, getShellPointer, menuState, nativePaneChrome, setHoveredMenuItemId, setMenuState, transientFocusActive, updateDividerPreview]);
152
158
 
153
159
  const handlePaneAction = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
154
160
  if (windowMode) return;
@@ -48,6 +48,8 @@ interface ShellPaneLayersProps {
48
48
  startNativeDockedDrag: (paneId: string, rect: LayoutBounds, event: any) => void;
49
49
  startNativeFloatingDrag: (paneId: string, rect: FloatingRect, event: any) => void;
50
50
  startNativeFloatResize: (paneId: string, rect: FloatingRect, event: any) => void;
51
+ transientFocusActive: boolean;
52
+ transientFocusPaneId: string | null;
51
53
  visibleFloatingPanes: VisibleFloatingPane[];
52
54
  width: number;
53
55
  windowModeDockResizePathKey: string | null;
@@ -79,6 +81,8 @@ export function ShellPaneLayers({
79
81
  startNativeDockedDrag,
80
82
  startNativeFloatingDrag,
81
83
  startNativeFloatResize,
84
+ transientFocusActive,
85
+ transientFocusPaneId,
82
86
  visibleFloatingPanes,
83
87
  width,
84
88
  windowModeDockResizePathKey,
@@ -87,8 +91,12 @@ export function ShellPaneLayers({
87
91
  return (
88
92
  <>
89
93
  {dockLeafLayouts.map((leaf) => {
94
+ if (transientFocusActive && leaf.instanceId !== transientFocusPaneId) return null;
90
95
  const pane = paneMap.get(leaf.instanceId);
91
96
  if (!pane) return null;
97
+ const rect = transientFocusActive
98
+ ? { x: 0, y: 0, width, height: contentHeight }
99
+ : leaf.rect;
92
100
  const focused = focusedPaneId === leaf.instanceId && (!overlayOpen || menuPaneId === leaf.instanceId);
93
101
  const windowModeSelected = windowModePaneId === leaf.instanceId;
94
102
  const showActions = focused || hoveredPaneId === leaf.instanceId || menuPaneId === leaf.instanceId;
@@ -96,17 +104,17 @@ export function ShellPaneLayers({
96
104
  <Box
97
105
  key={`dock:${leaf.instanceId}`}
98
106
  position="absolute"
99
- left={leaf.rect.x}
100
- top={leaf.rect.y}
101
- width={leaf.rect.width}
102
- height={leaf.rect.height}
107
+ left={rect.x}
108
+ top={rect.y}
109
+ width={rect.width}
110
+ height={rect.height}
103
111
  >
104
112
  <PaneFooterProvider>
105
113
  {(footer) => {
106
114
  const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
107
115
  const bodyFrame = resolvePaneBodyFrame({
108
- width: leaf.rect.width,
109
- height: leaf.rect.height,
116
+ width: rect.width,
117
+ height: rect.height,
110
118
  nativePaneChrome,
111
119
  reserveFooter,
112
120
  });
@@ -115,18 +123,18 @@ export function ShellPaneLayers({
115
123
  paneId={leaf.instanceId}
116
124
  title={getPaneTitle(pane)}
117
125
  focused={focused}
118
- width={leaf.rect.width}
119
- height={leaf.rect.height}
126
+ width={rect.width}
127
+ height={rect.height}
120
128
  showActions={showActions}
121
129
  windowModeSelected={windowModeSelected}
122
130
  footer={footer}
123
131
  onMouseDown={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
124
132
  onMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
125
- onHeaderMouseDown={nativePaneChrome ? (event) => startNativeDockedDrag(leaf.instanceId, leaf.rect, event) : undefined}
126
- onHeaderMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
127
- onHeaderMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
128
- onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, leaf.rect, event) : undefined}
129
- onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, leaf.rect, event)}
133
+ onHeaderMouseDown={nativePaneChrome && !transientFocusActive ? (event) => startNativeDockedDrag(leaf.instanceId, rect, event) : undefined}
134
+ onHeaderMouseDrag={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
135
+ onHeaderMouseDragEnd={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
136
+ onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, rect, event) : undefined}
137
+ onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, rect, event)}
130
138
  >
131
139
  <PaneContent
132
140
  component={pane.def.component}
@@ -145,7 +153,10 @@ export function ShellPaneLayers({
145
153
  })}
146
154
 
147
155
  {visibleFloatingPanes.map(({ pane, rect }) => {
148
- const preview = dragFloatingRect?.paneId === pane.instance.instanceId
156
+ if (transientFocusActive && pane.instance.instanceId !== transientFocusPaneId) return null;
157
+ const preview = transientFocusActive
158
+ ? { x: 0, y: 0, width, height: contentHeight }
159
+ : dragFloatingRect?.paneId === pane.instance.instanceId
149
160
  ? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
150
161
  : rect;
151
162
  const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuPaneId === pane.instance.instanceId);
@@ -203,6 +214,7 @@ export function ShellPaneLayers({
203
214
  })}
204
215
 
205
216
  {dockDividerLayouts.map((divider) => {
217
+ if (transientFocusActive) return null;
206
218
  const dividerPathKey = pathKey(divider.path);
207
219
  const previewActive = dividerPreview?.pathKey === dividerPathKey;
208
220
  const active = previewActive || windowModeDockResizePathKey === dividerPathKey;
@@ -25,6 +25,7 @@ interface ShellPaneManagementShortcutOptions {
25
25
  overlayOpen: boolean;
26
26
  popOutFocusedPane(): boolean;
27
27
  startWindowMode(paneId?: string, mode?: WindowEditMode): void;
28
+ toggleFocusedPaneFullscreen(): boolean;
28
29
  toggleFocusedPaneFloating(): boolean;
29
30
  }
30
31
 
@@ -42,6 +43,7 @@ export function useShellPaneManagementShortcuts({
42
43
  overlayOpen,
43
44
  popOutFocusedPane,
44
45
  startWindowMode,
46
+ toggleFocusedPaneFullscreen,
45
47
  toggleFocusedPaneFloating,
46
48
  }: ShellPaneManagementShortcutOptions): void {
47
49
  const doubleEscapeCloseRef = useRef(createDoubleEscapeCloseState());
@@ -53,6 +55,20 @@ export function useShellPaneManagementShortcuts({
53
55
  }, [overlayOpen]);
54
56
 
55
57
  useShortcut((event) => {
58
+ const shortcut = resolvePaneManagementShortcut(event);
59
+ if (shortcut === "toggle-fullscreen" && !overlayOpen) {
60
+ if (!inputCaptured || inputCaptureAllowsPaneManagementShortcut(shortcut, event)) {
61
+ if (hasActiveDrag()) {
62
+ cancelActiveDrag();
63
+ }
64
+ if (toggleFocusedPaneFullscreen()) {
65
+ event.preventDefault();
66
+ event.stopPropagation();
67
+ return;
68
+ }
69
+ }
70
+ }
71
+
56
72
  const isEscape = event.name === "escape" || event.name === "esc";
57
73
  if (isEscape) {
58
74
  const doubleEscapeState = doubleEscapeCloseRef.current;
@@ -91,6 +107,9 @@ export function useShellPaneManagementShortcuts({
91
107
  case "settings":
92
108
  handled = openFocusedPaneSettings();
93
109
  break;
110
+ case "toggle-fullscreen":
111
+ handled = toggleFocusedPaneFullscreen();
112
+ break;
94
113
  case "toggle-floating":
95
114
  handled = toggleFocusedPaneFloating();
96
115
  break;
@@ -2,6 +2,7 @@ import type { KeyEventLike } from "../../../react/input";
2
2
 
3
3
  export const PANE_MANAGEMENT_ACCELERATORS = {
4
4
  settings: "CmdOrCtrl+,",
5
+ fullscreen: "CmdOrCtrl+Shift+F",
5
6
  toggleFloating: "CmdOrCtrl+Shift+D",
6
7
  popOut: "CmdOrCtrl+Shift+O",
7
8
  copyScreenshot: "CmdOrCtrl+Shift+C",
@@ -15,6 +16,7 @@ export const PANE_MANAGEMENT_ACCELERATORS = {
15
16
 
16
17
  export type PaneManagementShortcut =
17
18
  | "settings"
19
+ | "toggle-fullscreen"
18
20
  | "toggle-floating"
19
21
  | "pop-out"
20
22
  | "copy-screenshot"
@@ -38,6 +40,7 @@ export function resolvePaneManagementShortcut(
38
40
  if (!shifted || event.alt) return null;
39
41
  if (name === "c") return "copy-screenshot";
40
42
  if (name === "d") return "toggle-floating";
43
+ if (name === "f") return "toggle-fullscreen";
41
44
  if (name === "o") return "pop-out";
42
45
  if (name === "l") return "layout-actions";
43
46
  if (name === "g") return "gridlock-all";
@@ -50,6 +53,7 @@ export function inputCaptureAllowsPaneManagementShortcut(
50
53
  shortcut: PaneManagementShortcut,
51
54
  event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
52
55
  ): boolean {
56
+ if (shortcut === "toggle-fullscreen") return event.meta || event.super === true;
53
57
  if (shortcut !== "close" && shortcut !== "close-all-floating") return false;
54
58
  return event.meta || event.super || event.targetEditable !== true;
55
59
  }
@@ -41,6 +41,7 @@ interface UseShellTerminalPointerRuntimeOptions {
41
41
  selectWindowModePane: (paneId: string) => void;
42
42
  setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
43
43
  setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
44
+ transientFocusActive: boolean;
44
45
  visibleFloatingPanes: VisibleFloatingPane[];
45
46
  width: number;
46
47
  windowMode: WindowEditState | null;
@@ -79,6 +80,7 @@ export function useShellTerminalPointerRuntime({
79
80
  selectWindowModePane,
80
81
  setHoveredMenuItemId,
81
82
  setMenuState,
83
+ transientFocusActive,
82
84
  visibleFloatingPanes,
83
85
  width,
84
86
  windowMode,
@@ -132,25 +134,27 @@ export function useShellTerminalPointerRuntime({
132
134
  return;
133
135
  }
134
136
 
135
- for (const divider of dockDividerLayouts) {
136
- if (!pointInRect(divider.rect, event.x, shellY)) continue;
137
- dragRef.current = {
138
- type: "divider",
139
- path: divider.path,
140
- axis: divider.axis,
141
- startX: preciseX,
142
- startY: preciseShellY,
143
- startRatio: divider.ratio,
144
- bounds: divider.bounds,
145
- };
146
- updateDividerPreview({
147
- pathKey: divider.path.join("."),
148
- rect: divider.rect,
149
- ratio: divider.ratio,
150
- });
151
- event.stopPropagation();
152
- event.preventDefault();
153
- return;
137
+ if (!transientFocusActive) {
138
+ for (const divider of dockDividerLayouts) {
139
+ if (!pointInRect(divider.rect, event.x, shellY)) continue;
140
+ dragRef.current = {
141
+ type: "divider",
142
+ path: divider.path,
143
+ axis: divider.axis,
144
+ startX: preciseX,
145
+ startY: preciseShellY,
146
+ startRatio: divider.ratio,
147
+ bounds: divider.bounds,
148
+ };
149
+ updateDividerPreview({
150
+ pathKey: divider.path.join("."),
151
+ rect: divider.rect,
152
+ ratio: divider.ratio,
153
+ });
154
+ event.stopPropagation();
155
+ event.preventDefault();
156
+ return;
157
+ }
154
158
  }
155
159
 
156
160
  for (const leaf of dockLeafLayouts) {
@@ -279,6 +283,11 @@ export function useShellTerminalPointerRuntime({
279
283
  event.preventDefault();
280
284
  return;
281
285
  }
286
+ if (relativeY === 0 && transientFocusActive) {
287
+ event.stopPropagation();
288
+ event.preventDefault();
289
+ return;
290
+ }
282
291
  if (relativeY === 0) {
283
292
  dragRef.current = {
284
293
  type: "pane-drag",
@@ -317,6 +326,7 @@ export function useShellTerminalPointerRuntime({
317
326
  selectWindowModePane,
318
327
  setHoveredMenuItemId,
319
328
  setMenuState,
329
+ transientFocusActive,
320
330
  updateDividerPreview,
321
331
  updateDragFloatingRect,
322
332
  visibleFloatingPanes,
@@ -13,6 +13,7 @@ import {
13
13
  applyWindowEditDirection,
14
14
  cycleWindowEditFocus,
15
15
  cycleWindowEditPane,
16
+ cycleWindowEditTarget,
16
17
  directionFromWindowEditKey,
17
18
  getWindowEditPaneIds,
18
19
  normalizeWindowEditFocus,
@@ -72,7 +73,10 @@ export function useShellWindowMode({
72
73
  );
73
74
  const windowModePaneId = windowMode?.paneId;
74
75
  const activeLayout = windowMode?.previewLayout ?? visibleLayout;
75
- const windowModePaneIds = useMemo(() => getWindowEditPaneIds(activeLayout), [activeLayout]);
76
+ const windowModePaneIds = useMemo(
77
+ () => getWindowEditPaneIds(activeLayout, bounds, dockGeometryOptions),
78
+ [activeLayout, bounds, dockGeometryOptions],
79
+ );
76
80
  const windowModeDockMovePreview = useMemo(
77
81
  () => resolveWindowEditDockMovePreview(windowMode, bounds, dockGeometryOptions),
78
82
  [bounds, dockGeometryOptions, windowMode],
@@ -210,12 +214,16 @@ export function useShellWindowMode({
210
214
  }
211
215
  } else if (name === "w") {
212
216
  setWindowMode((current) => current
213
- ? cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
217
+ ? current.mode === "move" && current.focus.kind === "dock-move"
218
+ ? cycleWindowEditTarget(current, bounds, dockGeometryOptions, event.shift ? -1 : 1)
219
+ : cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
214
220
  : current);
215
221
  } else if (name === "tab") {
216
222
  setWindowMode((current) => current
217
223
  ? current.mode === "move"
218
- ? cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
224
+ ? current.focus.kind === "dock-move"
225
+ ? cycleWindowEditTarget(current, bounds, dockGeometryOptions, event.shift ? -1 : 1)
226
+ : cycleWindowEditPane(current, windowModePaneIds, bounds, dockGeometryOptions, event.shift ? -1 : 1)
219
227
  : {
220
228
  ...current,
221
229
  focus: cycleWindowEditFocus(
@@ -40,6 +40,13 @@ interface ShellWindowModeOverlaysProps {
40
40
  windowModeDockMovePreview: WindowEditDockMovePreview | null;
41
41
  }
42
42
 
43
+ function getHighlightedPaneId(windowMode: WindowEditState | null, focusedPaneId: string | null): string | null {
44
+ if (windowMode?.mode === "move" && windowMode.focus.kind === "dock-move") {
45
+ return windowMode.focus.targetId;
46
+ }
47
+ return windowMode?.paneId ?? focusedPaneId;
48
+ }
49
+
43
50
  function windowModeBannerKey(windowMode: WindowEditState): string {
44
51
  const focusKey = windowMode.focus.kind === "dock-move"
45
52
  ? `${windowMode.focus.targetId}:${windowMode.focus.position}`
@@ -69,7 +76,7 @@ export function ShellWindowModeOverlays({
69
76
  windowMode,
70
77
  windowModeDockMovePreview,
71
78
  }: ShellWindowModeOverlaysProps) {
72
- const highlightedPaneId = windowMode?.paneId ?? focusedPaneId;
79
+ const highlightedPaneId = getHighlightedPaneId(windowMode, focusedPaneId);
73
80
  const highlightedFloatingPane = highlightedPaneId
74
81
  ? visibleFloatingPanes.find((entry) => entry.pane.instance.instanceId === highlightedPaneId)
75
82
  : null;
@@ -5,6 +5,8 @@ import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types
5
5
  import type { AppNotificationRequest } from "../../types/plugin";
6
6
  import { StatusBar } from "./status-bar";
7
7
  import { setSharedRegistryForTests } from "../../plugins/registry";
8
+ import { useEffect, useState } from "react";
9
+ import { TransientLayoutProvider, useTransientLayout } from "./transient-layout";
8
10
 
9
11
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
12
 
@@ -17,6 +19,37 @@ afterEach(() => {
17
19
  });
18
20
 
19
21
  describe("StatusBar", () => {
22
+ function SeedTransientLayout({
23
+ onActivate,
24
+ onDeactivate,
25
+ onExit,
26
+ }: {
27
+ onActivate?: () => void;
28
+ onDeactivate?: () => void;
29
+ onExit?: () => void;
30
+ }) {
31
+ const { setTransientLayout } = useTransientLayout();
32
+ const [active, setActive] = useState(true);
33
+ useEffect(() => {
34
+ setTransientLayout({
35
+ id: "pane-focus",
36
+ label: "^F Focus",
37
+ active,
38
+ onActivate: () => {
39
+ onActivate?.();
40
+ setActive(true);
41
+ },
42
+ onDeactivate: () => {
43
+ onDeactivate?.();
44
+ setActive(false);
45
+ },
46
+ onExit,
47
+ });
48
+ return () => setTransientLayout(null);
49
+ }, [active, onActivate, onDeactivate, onExit, setTransientLayout]);
50
+ return null;
51
+ }
52
+
20
53
  test("opens the command bar from the shortcut hint", async () => {
21
54
  const config = createDefaultConfig("/tmp/gloomberb-test");
22
55
  config.layouts = [{ name: "Home", layout: cloneLayout(config.layout) }];
@@ -45,6 +78,78 @@ describe("StatusBar", () => {
45
78
  expect(actions).toContainEqual({ type: "SET_COMMAND_BAR", open: true, query: "" });
46
79
  });
47
80
 
81
+ test("shows a transient focus layout tab without replacing saved layouts", async () => {
82
+ const config = createDefaultConfig("/tmp/gloomberb-transient-layout-test");
83
+ config.layouts = [
84
+ { name: "Default", layout: cloneLayout(config.layout) },
85
+ { name: "Monitor", layout: cloneLayout(config.layout) },
86
+ ];
87
+ const state = {
88
+ ...createInitialState(config),
89
+ statusBarVisible: true,
90
+ };
91
+ const actions: Array<{ type: string; index?: number }> = [];
92
+ let activateCount = 0;
93
+ let deactivateCount = 0;
94
+ let exitCount = 0;
95
+ const handleActivate = () => { activateCount += 1; };
96
+ const handleDeactivate = () => { deactivateCount += 1; };
97
+ const handleExit = () => { exitCount += 1; };
98
+
99
+ testSetup = await testRender(
100
+ <AppContext value={{ state, dispatch: (action) => actions.push(action as { type: string; index?: number }) }}>
101
+ <TransientLayoutProvider>
102
+ <SeedTransientLayout
103
+ onActivate={handleActivate}
104
+ onDeactivate={handleDeactivate}
105
+ onExit={handleExit}
106
+ />
107
+ <StatusBar />
108
+ </TransientLayoutProvider>
109
+ </AppContext>,
110
+ { width: 120, height: 1 },
111
+ );
112
+
113
+ await testSetup.renderOnce();
114
+ await testSetup.renderOnce();
115
+
116
+ const frame = testSetup.captureCharFrame();
117
+ expect(frame).toContain("^1 Default");
118
+ expect(frame).toContain("^2 Monitor");
119
+ expect(frame).toContain("^F Focus");
120
+
121
+ const monitorX = frame.split("\n")[0]?.indexOf("^2 Monitor") ?? -1;
122
+ expect(monitorX).toBeGreaterThanOrEqual(0);
123
+
124
+ await testSetup.mockMouse.click(monitorX + 1, 0);
125
+ await testSetup.renderOnce();
126
+ await testSetup.renderOnce();
127
+
128
+ expect(deactivateCount).toBe(1);
129
+ expect(exitCount).toBe(0);
130
+ expect(actions).toContainEqual({ type: "SWITCH_LAYOUT", index: 1 });
131
+
132
+ const afterSwitchFrame = testSetup.captureCharFrame();
133
+ expect(afterSwitchFrame).toContain("^F Focus");
134
+
135
+ const focusX = afterSwitchFrame.split("\n")[0]?.indexOf("^F Focus") ?? -1;
136
+ expect(focusX).toBeGreaterThanOrEqual(0);
137
+
138
+ await testSetup.mockMouse.click(focusX + 1, 0);
139
+ await testSetup.renderOnce();
140
+
141
+ expect(activateCount).toBe(1);
142
+
143
+ const activeFocusFrame = testSetup.captureCharFrame();
144
+ const activeFocusX = activeFocusFrame.split("\n")[0]?.indexOf("^F Focus") ?? -1;
145
+ expect(activeFocusX).toBeGreaterThanOrEqual(0);
146
+
147
+ await testSetup.mockMouse.click(activeFocusX + 1, 0);
148
+ await testSetup.renderOnce();
149
+
150
+ expect(exitCount).toBe(1);
151
+ });
152
+
48
153
  test("shows a gridlock tip after a corner snap and runs gridlock on click", async () => {
49
154
  const config = createDefaultConfig("/tmp/gloomberb-test");
50
155
  const floatingLayout = cloneLayout(config.layout);
@@ -16,6 +16,7 @@ import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
16
16
  import { PluginSlot } from "../../react/plugins/plugin-slot";
17
17
  import type { ContextMenuItem } from "../../types/context-menu";
18
18
  import { Tabs } from "../ui/tabs";
19
+ import { useTransientLayout } from "./transient-layout";
19
20
 
20
21
  const GRIDLOCK_TIP_DURATION_MS = 60_000;
21
22
 
@@ -31,6 +32,7 @@ type LayoutTabItem = {
31
32
 
32
33
  type StatusBarViewProps = {
33
34
  activeLayoutIdx: number;
35
+ activeLayoutValue: string;
34
36
  dismissGridlockTip: (event?: StatusBarEvent) => void;
35
37
  handleGridlockTip: (event?: StatusBarEvent) => void;
36
38
  handleLayoutSelect: (value: string) => void;
@@ -62,18 +64,40 @@ export function StatusBar() {
62
64
  const statusBarVisible = useAppSelector(selectStatusBarVisible);
63
65
  const gridlockTipVisible = useAppSelector(selectGridlockTipVisible);
64
66
  const gridlockTipSequence = useAppSelector(selectGridlockTipSequence);
67
+ const { transientLayout } = useTransientLayout();
65
68
  const [hoveredControl, setHoveredControl] = useState<string | null>(null);
66
69
 
67
- const hasMultipleLayouts = layouts.length > 1;
70
+ const hasMultipleLayouts = layouts.length > 1 || !!transientLayout;
68
71
  const showGridlockTip = gridlockTipVisible && !!registry;
69
- const layoutTabs = layouts.map((layout, index) => ({
72
+ const savedLayoutTabs = layouts.map((layout, index) => ({
70
73
  label: `^${index + 1} ${truncate(layout.name, 14)}`,
71
74
  value: String(index),
72
75
  }));
76
+ const layoutTabs = transientLayout
77
+ ? [
78
+ ...savedLayoutTabs,
79
+ {
80
+ label: transientLayout.label,
81
+ value: transientLayout.id,
82
+ },
83
+ ]
84
+ : savedLayoutTabs;
73
85
  const layoutTabsWidth = layoutTabs.reduce((sum, tab) => sum + tab.label.length + 2, 0);
86
+ const activeLayoutValue = transientLayout?.active ? transientLayout.id : String(activeLayoutIdx);
74
87
  const handleLayoutSelect = (value: string) => {
88
+ if (value === transientLayout?.id) {
89
+ if (transientLayout.active) {
90
+ transientLayout.onExit?.();
91
+ } else {
92
+ transientLayout.onActivate?.();
93
+ }
94
+ return;
95
+ }
75
96
  const index = Number(value);
76
97
  if (!Number.isInteger(index) || index < 0 || index >= layouts.length) return;
98
+ if (transientLayout?.active) {
99
+ transientLayout.onDeactivate?.();
100
+ }
77
101
  dispatch({ type: "SWITCH_LAYOUT", index });
78
102
  };
79
103
 
@@ -184,11 +208,12 @@ export function StatusBar() {
184
208
  );
185
209
  }, [activeLayoutIdx, layoutContextMenuItems, layouts, showContextMenu]);
186
210
  const handleLayoutTabContextMenu = useCallback((value: string, event: any) => {
211
+ if (value === transientLayout?.id) return;
187
212
  const index = Number(value);
188
213
  if (!Number.isInteger(index) || index < 0 || index >= layouts.length) return;
189
214
  if (event?.type !== "contextmenu" && event?.button === 2 && nativeContextMenu === true) return;
190
215
  void openLayoutContextMenu(index, event);
191
- }, [layouts.length, nativeContextMenu, openLayoutContextMenu]);
216
+ }, [layouts.length, nativeContextMenu, openLayoutContextMenu, transientLayout?.id]);
192
217
  const layoutTabItems = layoutTabs.map((tab) => ({
193
218
  ...tab,
194
219
  onContextMenu: handleLayoutTabContextMenu,
@@ -198,6 +223,7 @@ export function StatusBar() {
198
223
 
199
224
  const viewProps: StatusBarViewProps = {
200
225
  activeLayoutIdx,
226
+ activeLayoutValue,
201
227
  dismissGridlockTip,
202
228
  handleGridlockTip,
203
229
  handleLayoutSelect,
@@ -272,7 +298,7 @@ function TerminalStatusBar({
272
298
  }
273
299
 
274
300
  function StatusBarLayoutControl({
275
- activeLayoutIdx,
301
+ activeLayoutValue,
276
302
  handleLayoutSelect,
277
303
  hasMultipleLayouts,
278
304
  hoveredControl,
@@ -283,7 +309,7 @@ function StatusBarLayoutControl({
283
309
  setHoveredControl,
284
310
  }: Pick<
285
311
  StatusBarViewProps,
286
- | "activeLayoutIdx"
312
+ | "activeLayoutValue"
287
313
  | "handleLayoutSelect"
288
314
  | "hasMultipleLayouts"
289
315
  | "hoveredControl"
@@ -303,7 +329,7 @@ function StatusBarLayoutControl({
303
329
  <Box width={layoutTabsWidth} height={1}>
304
330
  <Tabs
305
331
  tabs={layoutTabItems}
306
- activeValue={String(activeLayoutIdx)}
332
+ activeValue={activeLayoutValue}
307
333
  onSelect={handleLayoutSelect}
308
334
  compact
309
335
  variant="pill"