gloomberb 0.9.2 → 0.9.4

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 +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -1,5 +1,5 @@
1
- import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost } from "../../../ui";
2
- import { useCallback, useMemo, useState } from "react";
1
+ import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost, type BoxRenderable } from "../../../ui";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useRendererHost, useUiCapabilities } from "../../../ui";
4
4
  import { useViewport } from "../../../react/input";
5
5
  import { useDialogState } from "../../../ui/dialog";
@@ -8,6 +8,7 @@ import type { DesktopDockPreviewState, DesktopWindowBridge } from "../../../type
8
8
  import {
9
9
  getDockDividerLayouts,
10
10
  getDockLeafLayouts,
11
+ isPaneInLayout,
11
12
  type DockGeometryOptions,
12
13
  type LayoutBounds,
13
14
  type ResolvedPane,
@@ -58,6 +59,12 @@ import {
58
59
  useShellVisibleLayout,
59
60
  } from "./layout-state";
60
61
  import { useShellPaneActions } from "./pane/actions";
62
+ import { resolvePaneFocusSourceLayout } from "./fullscreen";
63
+ import { useTransientLayout } from "../transient-layout";
64
+ import {
65
+ resolveShellCursorOcclusionRects,
66
+ useShellCursorOcclusionGuard,
67
+ } from "./cursor-occlusion";
61
68
 
62
69
  export { resolveAppHeaderHeightCells } from "./chrome";
63
70
  export { buildNativeWindowState } from "./native/window-state";
@@ -70,6 +77,13 @@ interface ShellProps {
70
77
  commandBarNativeOccluder?: LayoutBounds | null;
71
78
  }
72
79
 
80
+ interface TransientFocusLayoutState {
81
+ paneId: string;
82
+ layout: LayoutConfig;
83
+ sourceLayoutIndex: number;
84
+ active: boolean;
85
+ }
86
+
73
87
  export function Shell({
74
88
  pluginRegistry,
75
89
  desktopWindowBridge,
@@ -86,11 +100,13 @@ export function Shell({
86
100
  const inputCaptured = useAppSelector((state) => state.inputCaptured);
87
101
  const statusBarVisible = useAppSelector(selectStatusBarVisible);
88
102
  const rendererHost = useRendererHost();
103
+ const { setTransientLayout } = useTransientLayout();
89
104
  const uiKind = useUiHost().kind;
90
105
  const shortcutDisplayMode = getShortcutDisplayMode(uiKind);
91
106
  const { nativePaneChrome = false, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
92
107
  const { showContextMenu } = useContextMenu();
93
108
  const { width, height } = useViewport();
109
+ const shellRef = useRef<BoxRenderable | null>(null);
94
110
 
95
111
  const appHeaderHeight = resolveAppHeaderHeightCells({ titleBarOverlay, cellHeightPx });
96
112
  const contentHeight = Math.max(1, height - appHeaderHeight - (statusBarVisible ? 1 : 0));
@@ -103,6 +119,9 @@ export function Shell({
103
119
  setHoveredPaneId((current) => (current === paneId ? current : paneId));
104
120
  }, []);
105
121
  const [menuState, setMenuState] = useState<ActionMenuState | null>(null);
122
+ const [transientFocusLayoutState, setTransientFocusLayoutState] = useState<TransientFocusLayoutState | null>(null);
123
+ const transientFocusLayoutStateRef = useRef<TransientFocusLayoutState | null>(null);
124
+ transientFocusLayoutStateRef.current = transientFocusLayoutState;
106
125
  const [hoveredMenuItemId, setHoveredMenuItemId] = useState<string | null>(null);
107
126
  const closePaneMenu = useCallback(() => {
108
127
  setMenuState(null);
@@ -110,7 +129,11 @@ export function Shell({
110
129
  }, []);
111
130
  const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
112
131
 
113
- const dragRuntime = useShellDragRuntimeState({ contentHeight, width });
132
+ const dragRuntime = useShellDragRuntimeState({
133
+ contentHeight,
134
+ throttleFloatingPreview: nativePaneChrome,
135
+ width,
136
+ });
114
137
  const {
115
138
  cancelActiveDrag,
116
139
  dividerPreview,
@@ -149,7 +172,7 @@ export function Shell({
149
172
  }, [dispatch]);
150
173
 
151
174
  const {
152
- activeLayout,
175
+ activeLayout: windowModeLayout,
153
176
  nativeWindowModePanelRect,
154
177
  selectWindowModePane,
155
178
  startWindowMode,
@@ -171,6 +194,17 @@ export function Shell({
171
194
  visibleLayout,
172
195
  width,
173
196
  });
197
+ const transientFocusActive = !windowMode && transientFocusLayoutState?.active === true;
198
+ const activeLayout = transientFocusActive && transientFocusLayoutState
199
+ ? transientFocusLayoutState.layout
200
+ : windowModeLayout;
201
+ const transientFocusPaneId = transientFocusActive ? transientFocusLayoutState?.paneId ?? null : null;
202
+
203
+ useEffect(() => {
204
+ if (!windowMode || !transientFocusLayoutState) return;
205
+ transientFocusLayoutStateRef.current = null;
206
+ setTransientFocusLayoutState(null);
207
+ }, [transientFocusLayoutState, windowMode]);
174
208
 
175
209
  const {
176
210
  dockedPanes,
@@ -184,6 +218,27 @@ export function Shell({
184
218
  pluginRegistry,
185
219
  width,
186
220
  });
221
+ const cursorOcclusionRects = useMemo(() => resolveShellCursorOcclusionRects({
222
+ contentHeight,
223
+ dragFloatingRect,
224
+ nativePaneChrome,
225
+ overlayOpen,
226
+ transientFocusActive,
227
+ visibleFloatingPanes,
228
+ width,
229
+ }), [
230
+ contentHeight,
231
+ dragFloatingRect,
232
+ nativePaneChrome,
233
+ overlayOpen,
234
+ transientFocusActive,
235
+ visibleFloatingPanes,
236
+ width,
237
+ ]);
238
+ useShellCursorOcclusionGuard({
239
+ occlusionRects: cursorOcclusionRects,
240
+ shellRef,
241
+ });
187
242
 
188
243
  const {
189
244
  closeAllFloatingPanes,
@@ -211,6 +266,102 @@ export function Shell({
211
266
  visibleLayout,
212
267
  width,
213
268
  });
269
+ const setTransientFocusLayout = useCallback((next: TransientFocusLayoutState | null) => {
270
+ transientFocusLayoutStateRef.current = next;
271
+ setTransientFocusLayoutState(next);
272
+ }, []);
273
+ const activateTransientFocusState = useCallback((current: TransientFocusLayoutState) => {
274
+ closePaneMenu();
275
+ setTransientFocusLayout({ ...current, active: true });
276
+ const sourceLayout = config.layouts[current.sourceLayoutIndex]?.layout;
277
+ if (
278
+ current.sourceLayoutIndex !== config.activeLayoutIndex
279
+ && sourceLayout
280
+ && isPaneInLayout(sourceLayout, current.paneId)
281
+ ) {
282
+ dispatch({ type: "SWITCH_LAYOUT", index: current.sourceLayoutIndex });
283
+ }
284
+ focusPane(current.paneId);
285
+ }, [closePaneMenu, config.activeLayoutIndex, config.layouts, dispatch, focusPane, setTransientFocusLayout]);
286
+ const toggleFocusedPaneFullscreen = useCallback(() => {
287
+ const current = transientFocusLayoutStateRef.current;
288
+ if (current?.active) {
289
+ setTransientFocusLayout(null);
290
+ return true;
291
+ }
292
+
293
+ if (
294
+ current
295
+ && current.paneId === focusedPaneId
296
+ && current.sourceLayoutIndex === config.activeLayoutIndex
297
+ ) {
298
+ activateTransientFocusState(current);
299
+ return true;
300
+ }
301
+
302
+ const nextLayout = resolvePaneFocusSourceLayout(visibleLayout, focusedPaneId);
303
+ if (!focusedPaneId || !nextLayout) {
304
+ pluginRegistry.notify({ body: "Focus a pane to make it fullscreen", type: "info" });
305
+ return false;
306
+ }
307
+
308
+ closePaneMenu();
309
+ setTransientFocusLayout({
310
+ paneId: focusedPaneId,
311
+ layout: nextLayout,
312
+ sourceLayoutIndex: config.activeLayoutIndex,
313
+ active: true,
314
+ });
315
+ focusPane(focusedPaneId);
316
+ return true;
317
+ }, [
318
+ activateTransientFocusState,
319
+ closePaneMenu,
320
+ config.activeLayoutIndex,
321
+ focusedPaneId,
322
+ focusPane,
323
+ pluginRegistry,
324
+ setTransientFocusLayout,
325
+ visibleLayout,
326
+ ]);
327
+ const activateTransientFocusLayout = useCallback(() => {
328
+ const current = transientFocusLayoutStateRef.current;
329
+ if (!current) return;
330
+ activateTransientFocusState(current);
331
+ }, [activateTransientFocusState]);
332
+ const deactivateTransientFocusLayout = useCallback(() => {
333
+ const current = transientFocusLayoutStateRef.current;
334
+ if (!current || !current.active) return;
335
+ closePaneMenu();
336
+ setTransientFocusLayout({ ...current, active: false });
337
+ }, [closePaneMenu, setTransientFocusLayout]);
338
+ const exitTransientFocusLayout = useCallback(() => {
339
+ closePaneMenu();
340
+ setTransientFocusLayout(null);
341
+ }, [closePaneMenu, setTransientFocusLayout]);
342
+
343
+ useEffect(() => {
344
+ setTransientLayout(
345
+ transientFocusLayoutState
346
+ ? {
347
+ id: "pane-focus",
348
+ label: "^F Focus",
349
+ active: transientFocusActive,
350
+ onActivate: activateTransientFocusLayout,
351
+ onDeactivate: deactivateTransientFocusLayout,
352
+ onExit: exitTransientFocusLayout,
353
+ }
354
+ : null,
355
+ );
356
+ return () => setTransientLayout(null);
357
+ }, [
358
+ activateTransientFocusLayout,
359
+ deactivateTransientFocusLayout,
360
+ exitTransientFocusLayout,
361
+ setTransientLayout,
362
+ transientFocusActive,
363
+ transientFocusLayoutState,
364
+ ]);
214
365
 
215
366
  useShellPaneManagementShortcuts({
216
367
  cancelActiveDrag,
@@ -226,6 +377,7 @@ export function Shell({
226
377
  overlayOpen,
227
378
  popOutFocusedPane,
228
379
  startWindowMode,
380
+ toggleFocusedPaneFullscreen,
229
381
  toggleFocusedPaneFloating,
230
382
  });
231
383
 
@@ -333,6 +485,7 @@ export function Shell({
333
485
  closePaneMenu,
334
486
  contentHeight,
335
487
  dispatch,
488
+ dockGeometryOptions,
336
489
  dockDividerLayouts,
337
490
  dockLeafLayouts,
338
491
  dragRuntime,
@@ -349,6 +502,7 @@ export function Shell({
349
502
  setHoveredMenuItemId,
350
503
  setMenuState,
351
504
  snapGuides,
505
+ transientFocusActive,
352
506
  updateWindowModePreviewLayout,
353
507
  visibleFloatingPanes,
354
508
  visibleLayout,
@@ -361,8 +515,13 @@ export function Shell({
361
515
 
362
516
  return (
363
517
  <Box
518
+ ref={shellRef}
364
519
  flexDirection="row"
365
520
  flexGrow={1}
521
+ flexShrink={1}
522
+ flexBasis={0}
523
+ minWidth={0}
524
+ minHeight={0}
366
525
  height={nativePaneChrome ? undefined : contentHeight}
367
526
  position={nativePaneChrome ? "relative" : undefined}
368
527
  overflow="hidden"
@@ -409,6 +568,8 @@ export function Shell({
409
568
  startNativeDockedDrag={startNativeDockedDrag}
410
569
  startNativeFloatingDrag={startNativeFloatingDrag}
411
570
  startNativeFloatResize={startNativeFloatResize}
571
+ transientFocusActive={transientFocusActive}
572
+ transientFocusPaneId={transientFocusPaneId}
412
573
  visibleFloatingPanes={visibleFloatingPanes}
413
574
  width={width}
414
575
  windowModeDockResizePathKey={windowModeDockResizePathKey}
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getDockedPaneIds } from "../../../plugins/pane-manager";
3
+ import { createPaneInstance, type LayoutConfig } from "../../../types/config";
4
+ import { resolveShellVisibleLayout } from "./visible-layout";
5
+
6
+ describe("shell visible layout", () => {
7
+ test("removes unregistered pane types before resolving dock geometry", () => {
8
+ const layout: LayoutConfig = {
9
+ dockRoot: {
10
+ kind: "split",
11
+ axis: "horizontal",
12
+ ratio: 0.5,
13
+ first: { kind: "pane", instanceId: "missing:main" },
14
+ second: { kind: "pane", instanceId: "chat:main" },
15
+ },
16
+ instances: [
17
+ createPaneInstance("missing-plugin", { instanceId: "missing:main" }),
18
+ createPaneInstance("chat", { instanceId: "chat:main" }),
19
+ ],
20
+ floating: [],
21
+ detached: [],
22
+ };
23
+
24
+ const visibleLayout = resolveShellVisibleLayout(
25
+ layout,
26
+ new Set(),
27
+ new Map([["chat", true]]),
28
+ );
29
+
30
+ expect(visibleLayout.instances.map((instance) => instance.instanceId)).toEqual(["chat:main"]);
31
+ expect(getDockedPaneIds(visibleLayout)).toEqual(["chat:main"]);
32
+ });
33
+ });
@@ -6,8 +6,9 @@ import {
6
6
  type ResolvedPane,
7
7
  } from "../../../plugins/pane-manager";
8
8
  import type { PluginRegistry } from "../../../plugins/registry";
9
- import { removePaneInstances, type LayoutConfig } from "../../../types/config";
9
+ import type { LayoutConfig } from "../../../types/config";
10
10
  import { constrainFloatingRectToBounds } from "./drag";
11
+ import { resolveShellVisibleLayout } from "./visible-layout";
11
12
 
12
13
  interface ShellVisibleLayoutOptions {
13
14
  disabledPlugins: readonly string[];
@@ -34,15 +35,9 @@ export function useShellVisibleLayout({
34
35
  return paneIds;
35
36
  }, [disabledPlugins, pluginRegistry]);
36
37
 
37
- const hiddenInstanceIds = useMemo(() => (
38
- layout.instances
39
- .filter((instance) => disabledPaneIds.has(instance.paneId))
40
- .map((instance) => instance.instanceId)
41
- ), [disabledPaneIds, layout.instances]);
42
-
43
38
  const visibleLayout = useMemo(
44
- () => (hiddenInstanceIds.length > 0 ? removePaneInstances(layout, hiddenInstanceIds) : layout),
45
- [hiddenInstanceIds, layout],
39
+ () => resolveShellVisibleLayout(layout, disabledPaneIds, pluginRegistry.panes),
40
+ [disabledPaneIds, layout, pluginRegistry.panes],
46
41
  );
47
42
 
48
43
  return { disabledPaneIds, visibleLayout };
@@ -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,18 +104,21 @@ 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
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
114
+ const showFooter = hasPaneFooterContent(footer);
115
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
116
+ const renderFooter = reserveFooter || showFooter;
107
117
  const bodyFrame = resolvePaneBodyFrame({
108
- width: leaf.rect.width,
109
- height: leaf.rect.height,
118
+ width: rect.width,
119
+ height: rect.height,
110
120
  nativePaneChrome,
121
+ footerVisible: renderFooter,
111
122
  reserveFooter,
112
123
  });
113
124
  return (
@@ -115,18 +126,18 @@ export function ShellPaneLayers({
115
126
  paneId={leaf.instanceId}
116
127
  title={getPaneTitle(pane)}
117
128
  focused={focused}
118
- width={leaf.rect.width}
119
- height={leaf.rect.height}
129
+ width={rect.width}
130
+ height={rect.height}
120
131
  showActions={showActions}
121
132
  windowModeSelected={windowModeSelected}
122
133
  footer={footer}
123
134
  onMouseDown={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
124
135
  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)}
136
+ onHeaderMouseDown={nativePaneChrome && !transientFocusActive ? (event) => startNativeDockedDrag(leaf.instanceId, rect, event) : undefined}
137
+ onHeaderMouseDrag={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
138
+ onHeaderMouseDragEnd={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
139
+ onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, rect, event) : undefined}
140
+ onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, rect, event)}
130
141
  >
131
142
  <PaneContent
132
143
  component={pane.def.component}
@@ -145,7 +156,10 @@ export function ShellPaneLayers({
145
156
  })}
146
157
 
147
158
  {visibleFloatingPanes.map(({ pane, rect }) => {
148
- const preview = dragFloatingRect?.paneId === pane.instance.instanceId
159
+ if (transientFocusActive && pane.instance.instanceId !== transientFocusPaneId) return null;
160
+ const preview = transientFocusActive
161
+ ? { x: 0, y: 0, width, height: contentHeight }
162
+ : dragFloatingRect?.paneId === pane.instance.instanceId
149
163
  ? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
150
164
  : rect;
151
165
  const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuPaneId === pane.instance.instanceId);
@@ -154,11 +168,14 @@ export function ShellPaneLayers({
154
168
  return (
155
169
  <PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
156
170
  {(footer) => {
157
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
171
+ const showFooter = hasPaneFooterContent(footer);
172
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
173
+ const renderFooter = reserveFooter || showFooter;
158
174
  const bodyFrame = resolvePaneBodyFrame({
159
175
  width: preview.width,
160
176
  height: preview.height,
161
177
  nativePaneChrome,
178
+ footerVisible: renderFooter,
162
179
  reserveFooter,
163
180
  });
164
181
  return (
@@ -203,6 +220,7 @@ export function ShellPaneLayers({
203
220
  })}
204
221
 
205
222
  {dockDividerLayouts.map((divider) => {
223
+ if (transientFocusActive) return null;
206
224
  const dividerPathKey = pathKey(divider.path);
207
225
  const previewActive = dividerPreview?.pathKey === dividerPathKey;
208
226
  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
  }