gloomberb 0.9.3 → 0.9.5

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 (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -87,7 +87,11 @@ export function buildCurrentLayoutItems({
87
87
  kind: "action",
88
88
  action: () => {
89
89
  const { width, height } = pluginRegistry.getTermSizeFn();
90
- persistLayoutChange(gridlockAllPanes(currentLayout, { x: 0, y: 0, width, height }));
90
+ persistLayoutChange(gridlockAllPanes(
91
+ currentLayout,
92
+ { x: 0, y: 0, width, height },
93
+ pluginRegistry.panes,
94
+ ));
91
95
  notifyGridlockRevert();
92
96
  closeAll({ revertThemePreview: false });
93
97
  },
@@ -165,7 +165,7 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
165
165
  : isHovered
166
166
  ? paletteHoverBg
167
167
  : (nativePaneChrome ? panelBg : paletteBg)}
168
- onMouseMove={() => onHoverIndex(globalIdx)}
168
+ onMouseOver={() => onHoverIndex(globalIdx)}
169
169
  onMouseOut={() => onHoverIndex(null)}
170
170
  {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}
171
171
  onMouseDown={(event: any) => onRowMouseDown(event, item, globalIdx)}
@@ -219,7 +219,7 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
219
219
  : hovered
220
220
  ? paletteHoverBg
221
221
  : (nativePaneChrome ? panelBg : paletteBg)}
222
- onMouseMove={() => setHoveredIndex(index)}
222
+ onMouseOver={() => setHoveredIndex(index)}
223
223
  onMouseOut={() => setHoveredIndex(null)}
224
224
  onMouseDown={(event: any) => {
225
225
  event.stopPropagation?.();
@@ -73,8 +73,6 @@ export interface DataTableViewProps<
73
73
  | "scrollRef"
74
74
  | "syncHeaderScroll"
75
75
  | "onBodyScrollActivity"
76
- | "hoveredIdx"
77
- | "setHoveredIdx"
78
76
  | "isSelected"
79
77
  | "onSelect"
80
78
  | "onActivate"
@@ -97,8 +95,6 @@ export interface DataTableViewProps<
97
95
  scrollRef?: RefObject<ScrollBoxRenderable | null>;
98
96
  syncHeaderScroll?: () => void;
99
97
  onBodyScrollActivity?: () => void;
100
- hoveredIdx?: number | null;
101
- setHoveredIdx?: (index: number | null) => void;
102
98
  keyboardNavigation?: boolean;
103
99
  onRootKeyDown?: (
104
100
  event: DataTableKeyEvent,
@@ -125,8 +121,6 @@ export function DataTableView<
125
121
  scrollRef,
126
122
  syncHeaderScroll,
127
123
  onBodyScrollActivity,
128
- hoveredIdx,
129
- setHoveredIdx,
130
124
  keyboardNavigation = true,
131
125
  onRootKeyDown,
132
126
  resetScrollKey,
@@ -137,15 +131,11 @@ export function DataTableView<
137
131
  const {
138
132
  effectiveHeaderScrollRef,
139
133
  effectiveScrollRef,
140
- effectiveHoveredIdx,
141
- effectiveSetHoveredIdx,
142
134
  effectiveSyncHeaderScroll,
143
135
  } = useTableViewState({
144
136
  headerScrollRef,
145
137
  scrollRef,
146
138
  syncHeaderScroll,
147
- hoveredIdx,
148
- setHoveredIdx,
149
139
  });
150
140
  const [cursorIndex, setCursorIndex] = useState<number | null>(null);
151
141
  const pendingCommitRef = useRef(false);
@@ -532,8 +522,6 @@ export function DataTableView<
532
522
  scrollRef={effectiveScrollRef}
533
523
  syncHeaderScroll={effectiveSyncHeaderScroll}
534
524
  onBodyScrollActivity={handleBodyScrollActivity}
535
- hoveredIdx={effectiveHoveredIdx}
536
- setHoveredIdx={effectiveSetHoveredIdx}
537
525
  scrollToIndex={effectiveScrollToIndex}
538
526
  scrollToIndexVersion={scrollToIndexVersion + selectionScrollVersion}
539
527
  isSelected={isItemSelected}
@@ -10,7 +10,7 @@ import { useThemeColors } from "../../theme/theme-context";
10
10
  import { hasPaneFooterContent, PaneFooterBar, PaneFooterProvider } from "./pane/footer";
11
11
  import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
12
12
  import { PaneContent } from "./pane/content";
13
- import { resolvePaneBodyFrame } from "./pane/sizing";
13
+ import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./pane/sizing";
14
14
  import { getPaneDisplayTitle } from "./pane/title";
15
15
  import { TITLEBAR_OVERLAY_HEIGHT_PX, getTitlebarLeadingInset } from "./titlebar-overlay";
16
16
  import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
@@ -130,6 +130,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
130
130
  <PaneFooterProvider>
131
131
  {(footer) => {
132
132
  const showFooter = hasPaneFooterContent(footer);
133
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
134
+ const renderFooter = reserveFooter || showFooter;
133
135
  const headerHeightRows = titleBarOverlay ? TITLEBAR_OVERLAY_HEIGHT_PX / cellHeightPx : 1;
134
136
  const background = floatingPaneBg(focused);
135
137
  const titleBackground = floatingPaneTitleBg(focused);
@@ -137,7 +139,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
137
139
  width,
138
140
  height,
139
141
  nativePaneChrome,
140
- reserveFooter: showFooter,
142
+ footerVisible: renderFooter,
143
+ reserveFooter,
141
144
  headerRows: headerHeightRows,
142
145
  });
143
146
  const bodyWidth = bodyFrame.width ?? 1;
@@ -208,7 +211,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
208
211
  height={bodyHeight}
209
212
  />
210
213
  </PaneBodyFrame>
211
- {showFooter && <PaneFooterBar footer={footer} focused={focused} width={width} />}
214
+ {renderFooter && <PaneFooterBar footer={footer} focused={focused} width={width} />}
212
215
  </Box>
213
216
  );
214
217
  }}
@@ -18,7 +18,7 @@ interface FloatingPaneWrapperProps {
18
18
  windowModeSelected?: boolean;
19
19
  showActions?: boolean;
20
20
  onMouseDown?: (event: any) => void;
21
- onMouseMove?: (event: any) => void;
21
+ onHeaderMouseMove?: (event: any) => void;
22
22
  onHeaderMouseDown?: (event: any) => void;
23
23
  onHeaderMouseDrag?: (event: any) => void;
24
24
  onHeaderMouseDragEnd?: (event: any) => void;
@@ -63,7 +63,7 @@ export function FloatingPaneWrapper({
63
63
  windowModeSelected = false,
64
64
  showActions = false,
65
65
  onMouseDown,
66
- onMouseMove,
66
+ onHeaderMouseMove,
67
67
  onHeaderMouseDown,
68
68
  onHeaderMouseDrag,
69
69
  onHeaderMouseDragEnd,
@@ -80,7 +80,8 @@ export function FloatingPaneWrapper({
80
80
  const bg = floatingPaneBg(focused);
81
81
  const showFooter = hasPaneFooterContent(footer);
82
82
  const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
83
- const bodyFrame = resolvePaneBodyFrame({ height, nativePaneChrome, reserveFooter });
83
+ const renderFooter = reserveFooter || showFooter;
84
+ const bodyFrame = resolvePaneBodyFrame({ height, nativePaneChrome, footerVisible: renderFooter, reserveFooter });
84
85
 
85
86
  return (
86
87
  <Box
@@ -103,7 +104,6 @@ export function FloatingPaneWrapper({
103
104
  showBorderColor: true,
104
105
  })}
105
106
  onMouseDown={onMouseDown}
106
- onMouseMove={onMouseMove}
107
107
  >
108
108
  <PaneHeader
109
109
  title={title}
@@ -112,6 +112,7 @@ export function FloatingPaneWrapper({
112
112
  windowModeSelected={windowModeSelected}
113
113
  floating
114
114
  showActions={showActions}
115
+ onHeaderMouseMove={onHeaderMouseMove}
115
116
  onHeaderMouseDown={onHeaderMouseDown}
116
117
  onHeaderMouseDrag={onHeaderMouseDrag}
117
118
  onHeaderMouseDragEnd={onHeaderMouseDragEnd}
@@ -124,7 +125,7 @@ export function FloatingPaneWrapper({
124
125
  {children}
125
126
  </PaneBodyFrame>
126
127
 
127
- {reserveFooter && (
128
+ {renderFooter && (
128
129
  <PaneFooterBar
129
130
  footer={footer}
130
131
  focused={focused}
@@ -3,6 +3,7 @@ import { PaneInstanceProvider } from "../../../state/app/context";
3
3
  import { PaneKeyboardScrollController } from "../../../state/pane-scroll-registry";
4
4
  import { useThemeColors } from "../../../theme/theme-context";
5
5
  import type { PaneDef } from "../../../types/plugin";
6
+ import { Box } from "../../../ui";
6
7
 
7
8
  interface PaneContentProps {
8
9
  component: PaneDef["component"];
@@ -31,14 +32,25 @@ export const PaneContent = memo(function PaneContent({
31
32
  return (
32
33
  <PaneInstanceProvider paneId={paneId}>
33
34
  <PaneKeyboardScrollController paneId={paneId} focused={focused} />
34
- <Component
35
- paneId={paneId}
36
- paneType={paneType}
37
- focused={focused}
38
- width={width}
39
- height={height}
40
- close={onClose ? close : undefined}
41
- />
35
+ <Box
36
+ flexDirection="column"
37
+ flexGrow={1}
38
+ flexShrink={1}
39
+ flexBasis={0}
40
+ minWidth={0}
41
+ minHeight={0}
42
+ overflow="hidden"
43
+ data-gloom-role="pane-content"
44
+ >
45
+ <Component
46
+ paneId={paneId}
47
+ paneType={paneType}
48
+ focused={focused}
49
+ width={width}
50
+ height={height}
51
+ close={onClose ? close : undefined}
52
+ />
53
+ </Box>
42
54
  </PaneInstanceProvider>
43
55
  );
44
56
  });
@@ -3,6 +3,7 @@ import {
3
3
  useCallback,
4
4
  useContext,
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useMemo,
7
8
  useRef,
8
9
  useState,
@@ -17,6 +18,9 @@ import {
17
18
  type PaneHint,
18
19
  } from "./model";
19
20
 
21
+ const usePaneFooterRegistrationEffect =
22
+ typeof document === "undefined" ? useEffect : useLayoutEffect;
23
+
20
24
  interface PaneFooterContextValue {
21
25
  register(registrationId: string, registration: PaneFooterRegistration | null): void;
22
26
  unregister(registrationId: string): void;
@@ -85,14 +89,14 @@ export function usePaneFooter(
85
89
  const context = useContext(PaneFooterContext);
86
90
  const previousRegistrationRef = useRef<PaneFooterRegistration | null>(null);
87
91
 
88
- useEffect(() => {
92
+ usePaneFooterRegistrationEffect(() => {
89
93
  return () => {
90
94
  previousRegistrationRef.current = null;
91
95
  context?.unregister(registrationId);
92
96
  };
93
97
  }, [context, registrationId]);
94
98
 
95
- useEffect(() => {
99
+ usePaneFooterRegistrationEffect(() => {
96
100
  if (!context) return;
97
101
  const nextRegistration = factory() ?? null;
98
102
  if (samePaneFooterRegistration(previousRegistrationRef.current, nextRegistration)) return;
@@ -19,13 +19,13 @@ export function getPaneWindowAttributes({
19
19
  windowModeSelected?: boolean;
20
20
  showBorderColor?: boolean;
21
21
  }): Record<string, unknown> {
22
- if (!enabled) return {};
23
-
24
22
  const attributes: Record<string, unknown> = {
25
23
  "data-gloom-role": role,
26
- "data-focused": focused ? "true" : "false",
27
24
  };
28
25
  if (paneId) attributes["data-gloom-pane-id"] = paneId;
26
+ if (!enabled) return attributes;
27
+
28
+ attributes["data-focused"] = focused ? "true" : "false";
29
29
  if (floating != null) attributes["data-floating"] = floating ? "true" : "false";
30
30
  if (windowModeSelected != null) {
31
31
  attributes["data-window-mode-selected"] = windowModeSelected ? "true" : "false";
@@ -48,7 +48,7 @@ export function PaneBodyFrame({
48
48
  children: ReactNode;
49
49
  }) {
50
50
  return (
51
- <Box {...layoutProps} overflow="hidden" backgroundColor={backgroundColor}>
51
+ <Box {...layoutProps} overflow="hidden" backgroundColor={backgroundColor} data-gloom-role="pane-body">
52
52
  {children}
53
53
  </Box>
54
54
  );
@@ -1,5 +1,5 @@
1
- import { Box, Span, Text, useUiCapabilities } from "../../../ui";
2
- import type { ReactNode } from "react";
1
+ import { Box, Span, Text, useNativeRenderer, useUiCapabilities } from "../../../ui";
2
+ import { useCallback, useRef, type ReactNode } from "react";
3
3
  import { blendHex, colors, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "../../../theme/colors";
4
4
 
5
5
  const PANE_HEADER_HEIGHT = 1;
@@ -14,6 +14,7 @@ interface PaneHeaderProps {
14
14
  windowModeSelected?: boolean;
15
15
  floating?: boolean;
16
16
  showActions?: boolean;
17
+ onHeaderMouseMove?: (event: any) => void;
17
18
  onHeaderMouseDown?: (event: any) => void;
18
19
  onHeaderMouseDrag?: (event: any) => void;
19
20
  onHeaderMouseDragEnd?: (event: any) => void;
@@ -29,6 +30,18 @@ function truncateTitle(title: string, maxWidth: number): string {
29
30
  return `${title.slice(0, maxWidth - 2)}..`;
30
31
  }
31
32
 
33
+ function captureTerminalPointerDrag(renderer: unknown, renderable: unknown): void {
34
+ if (!renderable) return;
35
+ const hostCapture = (renderer as { captureMouseRenderable?: (target: unknown) => void }).captureMouseRenderable;
36
+ if (typeof hostCapture === "function") {
37
+ hostCapture.call(renderer, renderable);
38
+ return;
39
+ }
40
+ const capture = (renderer as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
41
+ if (typeof capture !== "function") return;
42
+ capture.call(renderer, renderable);
43
+ }
44
+
32
45
  function DesktopPaneButton({
33
46
  icon,
34
47
  onMouseDown,
@@ -99,6 +112,7 @@ export function PaneHeader({
99
112
  windowModeSelected = false,
100
113
  floating = false,
101
114
  showActions = false,
115
+ onHeaderMouseMove,
102
116
  onHeaderMouseDown,
103
117
  onHeaderMouseDrag,
104
118
  onHeaderMouseDragEnd,
@@ -107,11 +121,17 @@ export function PaneHeader({
107
121
  onCloseMouseDown,
108
122
  }: PaneHeaderProps) {
109
123
  const { nativePaneChrome } = useUiCapabilities();
124
+ const nativeRenderer = useNativeRenderer();
125
+ const terminalHeaderRef = useRef<unknown>(null);
110
126
  const visuallyFocused = focused || windowModeSelected;
111
127
  const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
112
128
  const actionText = showActions ? PANE_HEADER_ACTION : " ";
113
129
  const closeText = floating ? PANE_HEADER_CLOSE : "";
114
130
  const textColor = paneTitleText(visuallyFocused, floating);
131
+ const handleTerminalHeaderMouseDown = useCallback((event: any) => {
132
+ captureTerminalPointerDrag(nativeRenderer, terminalHeaderRef.current);
133
+ onHeaderMouseDown?.(event);
134
+ }, [nativeRenderer, onHeaderMouseDown]);
115
135
 
116
136
  if (nativePaneChrome) {
117
137
  return (
@@ -125,6 +145,7 @@ export function PaneHeader({
125
145
  data-focused={focused ? "true" : "false"}
126
146
  data-window-mode-selected={windowModeSelected ? "true" : "false"}
127
147
  onMouseDown={onHeaderMouseDown}
148
+ onMouseMove={onHeaderMouseMove}
128
149
  onMouseDrag={onHeaderMouseDrag}
129
150
  onMouseDragEnd={onHeaderMouseDragEnd}
130
151
  onContextMenu={onHeaderContextMenu}
@@ -201,7 +222,17 @@ export function PaneHeader({
201
222
  const fill = "─".repeat(fillLen);
202
223
 
203
224
  return (
204
- <Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
225
+ <Box
226
+ ref={terminalHeaderRef}
227
+ height={PANE_HEADER_HEIGHT}
228
+ width={width}
229
+ backgroundColor={backgroundColor}
230
+ flexDirection="row"
231
+ onMouseDown={handleTerminalHeaderMouseDown}
232
+ onMouseMove={onHeaderMouseMove}
233
+ onMouseDrag={onHeaderMouseDrag}
234
+ onMouseDragEnd={onHeaderMouseDragEnd}
235
+ >
205
236
  <Text fg={borderColor} selectable={false}>{"┌─"}</Text>
206
237
  <Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
207
238
  <Text fg={borderColor} selectable={false}>{fill}</Text>
@@ -229,7 +260,17 @@ export function PaneHeader({
229
260
  const padding = " ".repeat(Math.max(0, titleWidth - clippedTitle.length));
230
261
 
231
262
  return (
232
- <Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
263
+ <Box
264
+ ref={terminalHeaderRef}
265
+ height={PANE_HEADER_HEIGHT}
266
+ width={width}
267
+ backgroundColor={backgroundColor}
268
+ flexDirection="row"
269
+ onMouseDown={handleTerminalHeaderMouseDown}
270
+ onMouseMove={onHeaderMouseMove}
271
+ onMouseDrag={onHeaderMouseDrag}
272
+ onMouseDragEnd={onHeaderMouseDragEnd}
273
+ >
233
274
  <Text fg={textColor} selectable={false}>
234
275
  {`${PANE_HEADER_GRIP}${clippedTitle}${padding}`}
235
276
  </Text>
@@ -16,7 +16,7 @@ interface PaneWrapperProps {
16
16
  flexGrow?: number;
17
17
  showActions?: boolean;
18
18
  onMouseDown?: (event: any) => void;
19
- onMouseMove?: (event: any) => void;
19
+ onHeaderMouseMove?: (event: any) => void;
20
20
  onHeaderMouseDown?: (event: any) => void;
21
21
  onHeaderMouseDrag?: (event: any) => void;
22
22
  onHeaderMouseDragEnd?: (event: any) => void;
@@ -36,7 +36,7 @@ export function PaneWrapper({
36
36
  flexGrow,
37
37
  showActions = false,
38
38
  onMouseDown,
39
- onMouseMove,
39
+ onHeaderMouseMove,
40
40
  onHeaderMouseDown,
41
41
  onHeaderMouseDrag,
42
42
  onHeaderMouseDragEnd,
@@ -49,9 +49,11 @@ export function PaneWrapper({
49
49
  const bg = paneBg(focused);
50
50
  const showFooter = hasPaneFooterContent(footer);
51
51
  const reserveFooter = !!title && shouldReservePaneFooter(nativePaneChrome, showFooter);
52
+ const renderFooter = !!title && (reserveFooter || showFooter);
52
53
  const bodyFrame = resolvePaneBodyFrame({
53
54
  height: typeof height === "number" ? height : undefined,
54
55
  nativePaneChrome,
56
+ footerVisible: renderFooter,
55
57
  reserveFooter,
56
58
  headerRows: title ? 1 : 0,
57
59
  });
@@ -74,7 +76,6 @@ export function PaneWrapper({
74
76
  showBorderColor: true,
75
77
  })}
76
78
  onMouseDown={onMouseDown}
77
- onMouseMove={onMouseMove}
78
79
  >
79
80
  {title && (
80
81
  <PaneHeader
@@ -83,6 +84,7 @@ export function PaneWrapper({
83
84
  focused={focused}
84
85
  windowModeSelected={windowModeSelected}
85
86
  showActions={showActions}
87
+ onHeaderMouseMove={onHeaderMouseMove}
86
88
  onHeaderMouseDown={onHeaderMouseDown}
87
89
  onHeaderMouseDrag={onHeaderMouseDrag}
88
90
  onHeaderMouseDragEnd={onHeaderMouseDragEnd}
@@ -93,7 +95,7 @@ export function PaneWrapper({
93
95
  <PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={bg}>
94
96
  {children}
95
97
  </PaneBodyFrame>
96
- {title && reserveFooter && (
98
+ {renderFooter && (
97
99
  <PaneFooterBar
98
100
  footer={footer}
99
101
  focused={focused}
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./sizing";
3
+
4
+ describe("pane sizing", () => {
5
+ test("lets native pane chrome lay out footer bars in normal flex flow", () => {
6
+ expect(shouldReservePaneFooter(true, true)).toBe(false);
7
+
8
+ const bodyFrame = resolvePaneBodyFrame({
9
+ width: 80,
10
+ height: 30,
11
+ nativePaneChrome: true,
12
+ footerVisible: true,
13
+ reserveFooter: false,
14
+ });
15
+
16
+ expect(bodyFrame.height).toBe(28);
17
+ expect(bodyFrame.layoutProps).toEqual({
18
+ flexGrow: 1,
19
+ flexShrink: 1,
20
+ flexBasis: 0,
21
+ minWidth: 0,
22
+ minHeight: 0,
23
+ });
24
+ });
25
+
26
+ test("keeps terminal pane footer rows reserved for border rendering", () => {
27
+ expect(shouldReservePaneFooter(false, false)).toBe(true);
28
+
29
+ const bodyFrame = resolvePaneBodyFrame({
30
+ width: 80,
31
+ height: 30,
32
+ nativePaneChrome: false,
33
+ reserveFooter: true,
34
+ });
35
+
36
+ expect(bodyFrame.height).toBe(28);
37
+ expect(bodyFrame.layoutProps).toEqual({
38
+ height: 28,
39
+ flexGrow: 0,
40
+ flexBasis: undefined,
41
+ });
42
+ });
43
+ });
@@ -4,28 +4,35 @@ const PANE_FOOTER_ROWS = 1;
4
4
 
5
5
  const NATIVE_PANE_BODY_LAYOUT_PROPS = {
6
6
  flexGrow: 1,
7
+ flexShrink: 1,
7
8
  flexBasis: 0,
9
+ minWidth: 0,
8
10
  minHeight: 0,
9
11
  } as const;
10
12
 
11
- export function shouldReservePaneFooter(nativePaneChrome: boolean | undefined, showFooter: boolean): boolean {
12
- return !nativePaneChrome || showFooter;
13
+ export function shouldReservePaneFooter(nativePaneChrome: boolean | undefined, _showFooter: boolean): boolean {
14
+ return !nativePaneChrome;
13
15
  }
14
16
 
15
17
  function resolvePaneBodyHeight({
16
18
  height,
17
19
  nativePaneChrome,
20
+ footerVisible,
18
21
  reserveFooter = true,
19
22
  headerRows = PANE_HEADER_ROWS,
20
23
  }: {
21
24
  height: number;
22
25
  nativePaneChrome?: boolean;
26
+ footerVisible?: boolean;
23
27
  reserveFooter?: boolean;
24
28
  headerRows?: number;
25
29
  }): number {
26
30
  const finiteHeight = Number.isFinite(height) ? height : 1;
27
31
  const normalizedHeight = nativePaneChrome ? finiteHeight : Math.max(1, Math.floor(finiteHeight));
28
- return Math.max(1, normalizedHeight - headerRows - (reserveFooter ? PANE_FOOTER_ROWS : 0));
32
+ const footerRows = nativePaneChrome
33
+ ? footerVisible ? PANE_FOOTER_ROWS : 0
34
+ : reserveFooter ? PANE_FOOTER_ROWS : 0;
35
+ return Math.max(1, normalizedHeight - headerRows - footerRows);
29
36
  }
30
37
 
31
38
  function getPaneBodyLayoutProps(nativePaneChrome: boolean | undefined, bodyHeight: number | undefined) {
@@ -41,17 +48,19 @@ export function resolvePaneBodyFrame({
41
48
  width,
42
49
  height,
43
50
  nativePaneChrome,
51
+ footerVisible,
44
52
  reserveFooter = true,
45
53
  headerRows = PANE_HEADER_ROWS,
46
54
  }: {
47
55
  width?: number;
48
56
  height?: number;
49
57
  nativePaneChrome?: boolean;
58
+ footerVisible?: boolean;
50
59
  reserveFooter?: boolean;
51
60
  headerRows?: number;
52
61
  }) {
53
62
  const bodyHeight = typeof height === "number"
54
- ? resolvePaneBodyHeight({ height, nativePaneChrome, reserveFooter, headerRows })
63
+ ? resolvePaneBodyHeight({ height, nativePaneChrome, footerVisible, reserveFooter, headerRows })
55
64
  : undefined;
56
65
  return {
57
66
  width: typeof width === "number" ? resolvePaneBodyWidth(width, nativePaneChrome) : undefined,
@@ -52,7 +52,7 @@ export function ShellActionMenuOverlay({
52
52
  height={1}
53
53
  width={innerWidth}
54
54
  backgroundColor={hovered ? colors.selected : colors.panel}
55
- onMouseMove={() => onHoverItem(item.id)}
55
+ onMouseOver={() => onHoverItem(item.id)}
56
56
  onMouseDown={(mouseEvent: any) => {
57
57
  mouseEvent.stopPropagation();
58
58
  mouseEvent.preventDefault();