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
@@ -0,0 +1,204 @@
1
+ import { useEffect, useRef, type RefObject } from "react";
2
+ import { useNativeRenderer, type NativeCursorState, type NativePostProcessFn } from "../../../ui";
3
+ import type { FloatingRect, LayoutBounds, ResolvedPane } from "../../../plugins/pane-manager";
4
+ import { constrainFloatingRectToBounds } from "./drag";
5
+
6
+ interface VisibleFloatingPane {
7
+ pane: ResolvedPane;
8
+ rect: FloatingRect;
9
+ }
10
+
11
+ export interface ShellCursorOcclusionRect extends LayoutBounds {
12
+ paneId: string;
13
+ zIndex: number;
14
+ }
15
+
16
+ interface ResolveShellCursorOcclusionRectsOptions {
17
+ contentHeight: number;
18
+ dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
19
+ nativePaneChrome: boolean;
20
+ overlayOpen: boolean;
21
+ transientFocusActive: boolean;
22
+ visibleFloatingPanes: readonly VisibleFloatingPane[];
23
+ width: number;
24
+ }
25
+
26
+ const MAX_RENDERABLE_ANCESTOR_DEPTH = 256;
27
+
28
+ export function resolveShellCursorOcclusionRects({
29
+ contentHeight,
30
+ dragFloatingRect,
31
+ nativePaneChrome,
32
+ overlayOpen,
33
+ transientFocusActive,
34
+ visibleFloatingPanes,
35
+ width,
36
+ }: ResolveShellCursorOcclusionRectsOptions): ShellCursorOcclusionRect[] {
37
+ if (nativePaneChrome || overlayOpen || transientFocusActive || visibleFloatingPanes.length === 0) {
38
+ return [];
39
+ }
40
+
41
+ return visibleFloatingPanes.map(({ pane, rect }) => {
42
+ const paneId = pane.instance.instanceId;
43
+ const visibleRect = dragFloatingRect?.paneId === paneId
44
+ ? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
45
+ : rect;
46
+ return {
47
+ paneId,
48
+ zIndex: pane.floating?.zIndex ?? 50,
49
+ x: Math.floor(visibleRect.x),
50
+ y: Math.floor(visibleRect.y),
51
+ width: Math.max(0, Math.ceil(visibleRect.width)),
52
+ height: Math.max(0, Math.ceil(visibleRect.height)),
53
+ };
54
+ });
55
+ }
56
+
57
+ function readRenderableBounds(renderable: unknown): LayoutBounds | null {
58
+ if (!renderable || typeof renderable !== "object") return null;
59
+ const record = renderable as Record<string, unknown>;
60
+ const getRect = record.getBoundingClientRect;
61
+ if (typeof getRect === "function") {
62
+ const rect = getRect.call(renderable) as Partial<LayoutBounds> | null | undefined;
63
+ if (
64
+ rect
65
+ && typeof rect.x === "number"
66
+ && typeof rect.y === "number"
67
+ && typeof rect.width === "number"
68
+ && typeof rect.height === "number"
69
+ ) {
70
+ return rect as LayoutBounds;
71
+ }
72
+ }
73
+
74
+ const absoluteBounds = record.absoluteBounds as Partial<LayoutBounds> | null | undefined;
75
+ if (
76
+ absoluteBounds
77
+ && typeof absoluteBounds.x === "number"
78
+ && typeof absoluteBounds.y === "number"
79
+ && typeof absoluteBounds.width === "number"
80
+ && typeof absoluteBounds.height === "number"
81
+ ) {
82
+ return absoluteBounds as LayoutBounds;
83
+ }
84
+
85
+ const x = typeof record.screenX === "number"
86
+ ? record.screenX
87
+ : typeof record.absoluteX === "number"
88
+ ? record.absoluteX
89
+ : typeof record.x === "number"
90
+ ? record.x
91
+ : null;
92
+ const y = typeof record.screenY === "number"
93
+ ? record.screenY
94
+ : typeof record.absoluteY === "number"
95
+ ? record.absoluteY
96
+ : typeof record.y === "number"
97
+ ? record.y
98
+ : null;
99
+ const width = typeof record.width === "number" ? record.width : null;
100
+ const height = typeof record.height === "number" ? record.height : null;
101
+ if (x == null || y == null || width == null || height == null) return null;
102
+ return { x, y, width, height };
103
+ }
104
+
105
+ function readRenderablePaneId(renderable: unknown): string | null {
106
+ let current = renderable;
107
+ const seen = new Set<object>();
108
+ let depth = 0;
109
+
110
+ while (current && typeof current === "object" && !seen.has(current)) {
111
+ if (depth >= MAX_RENDERABLE_ANCESTOR_DEPTH) return null;
112
+ seen.add(current);
113
+ depth += 1;
114
+ const record = current as Record<string, unknown>;
115
+ const paneId = record["data-gloom-pane-id"];
116
+ if (typeof paneId === "string") return paneId;
117
+ current = record.parent;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ function rectContainsCursor(rect: LayoutBounds, cursor: NativeCursorState): boolean {
123
+ return cursor.x >= rect.x
124
+ && cursor.x < rect.x + rect.width
125
+ && cursor.y >= rect.y
126
+ && cursor.y < rect.y + rect.height;
127
+ }
128
+
129
+ function toTerminalRect(shellBounds: LayoutBounds, rect: ShellCursorOcclusionRect): ShellCursorOcclusionRect {
130
+ return {
131
+ ...rect,
132
+ x: Math.floor(shellBounds.x + rect.x + 1),
133
+ y: Math.floor(shellBounds.y + rect.y + 1),
134
+ };
135
+ }
136
+
137
+ function resolveCursorOwnerZIndex(
138
+ cursorOwnerPaneId: string | null,
139
+ rects: readonly ShellCursorOcclusionRect[],
140
+ ): number {
141
+ if (!cursorOwnerPaneId) return Number.NEGATIVE_INFINITY;
142
+ return rects.find((rect) => rect.paneId === cursorOwnerPaneId)?.zIndex ?? 0;
143
+ }
144
+
145
+ function shouldHideCursor({
146
+ cursor,
147
+ cursorOwnerPaneId,
148
+ terminalRects,
149
+ }: {
150
+ cursor: NativeCursorState;
151
+ cursorOwnerPaneId: string | null;
152
+ terminalRects: readonly ShellCursorOcclusionRect[];
153
+ }): boolean {
154
+ const ownerZIndex = resolveCursorOwnerZIndex(cursorOwnerPaneId, terminalRects);
155
+ return terminalRects.some((rect) => {
156
+ if (rect.paneId === cursorOwnerPaneId) return false;
157
+ if (rect.zIndex <= ownerZIndex) return false;
158
+ return rectContainsCursor(rect, cursor);
159
+ });
160
+ }
161
+
162
+ export function useShellCursorOcclusionGuard({
163
+ occlusionRects,
164
+ shellRef,
165
+ }: {
166
+ occlusionRects: readonly ShellCursorOcclusionRect[];
167
+ shellRef: RefObject<unknown>;
168
+ }) {
169
+ const nativeRenderer = useNativeRenderer();
170
+ const stateRef = useRef({ occlusionRects, shellRef });
171
+ stateRef.current = { occlusionRects, shellRef };
172
+
173
+ useEffect(() => {
174
+ if (
175
+ !nativeRenderer.addPostProcessFn
176
+ || !nativeRenderer.removePostProcessFn
177
+ || !nativeRenderer.getCursorState
178
+ || !nativeRenderer.setCursorPosition
179
+ ) {
180
+ return;
181
+ }
182
+
183
+ const guardCursor: NativePostProcessFn = () => {
184
+ const cursor = nativeRenderer.getCursorState?.();
185
+ if (!cursor?.visible) return;
186
+ const shellBounds = readRenderableBounds(stateRef.current.shellRef.current);
187
+ if (!shellBounds) return;
188
+
189
+ const terminalRects = stateRef.current.occlusionRects.map((rect) => toTerminalRect(shellBounds, rect));
190
+ if (terminalRects.length === 0) return;
191
+
192
+ const cursorOwnerPaneId = readRenderablePaneId(nativeRenderer.currentFocusedEditor);
193
+ if (!shouldHideCursor({ cursor, cursorOwnerPaneId, terminalRects })) return;
194
+ nativeRenderer.setCursorPosition?.(0, 0, false);
195
+ };
196
+
197
+ nativeRenderer.addPostProcessFn(guardCursor);
198
+ nativeRenderer.requestRender();
199
+ return () => {
200
+ nativeRenderer.removePostProcessFn?.(guardCursor);
201
+ nativeRenderer.requestRender();
202
+ };
203
+ }, [nativeRenderer]);
204
+ }
@@ -86,9 +86,11 @@ export interface ShellDragRuntimeState {
86
86
 
87
87
  export function useShellDragRuntimeState({
88
88
  contentHeight,
89
+ throttleFloatingPreview = false,
89
90
  width,
90
91
  }: {
91
92
  contentHeight: number;
93
+ throttleFloatingPreview?: boolean;
92
94
  width: number;
93
95
  }): ShellDragRuntimeState {
94
96
  const dragRef = useRef<DragMode | null>(null);
@@ -104,15 +106,20 @@ export function useShellDragRuntimeState({
104
106
  });
105
107
 
106
108
  const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
107
- pendingDragFloatingRectRef.current = next
109
+ const constrainedNext = next
108
110
  ? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
109
111
  : null;
112
+ pendingDragFloatingRectRef.current = constrainedNext;
110
113
  if (!next) {
111
114
  setDragFloatingRect(null);
112
115
  return;
113
116
  }
117
+ if (!throttleFloatingPreview) {
118
+ setDragFloatingRect(constrainedNext);
119
+ return;
120
+ }
114
121
  flushDragFloatingRect();
115
- }, [contentHeight, flushDragFloatingRect, width]);
122
+ }, [contentHeight, flushDragFloatingRect, throttleFloatingPreview, width]);
116
123
 
117
124
  const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
118
125
  dividerPreviewRef.current = next;
@@ -13,6 +13,7 @@ import {
13
13
  import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../../types/config";
14
14
  import type { PluginRegistry } from "../../../plugins/registry";
15
15
  import type { PaneProps } from "../../../types/plugin";
16
+ import { Textarea } from "../../../ui";
16
17
  import { Header } from "../header";
17
18
  import {
18
19
  buildNativeWindowState,
@@ -222,7 +223,7 @@ describe("Header", () => {
222
223
  );
223
224
 
224
225
  await testSetup.renderOnce();
225
- const frame = testSetup.captureCharFrame();
226
+ const frame = testSetup!.captureCharFrame();
226
227
 
227
228
  expect(frame).toContain("v0.3.0 available");
228
229
  expect(frame).toContain("starting download");
@@ -242,7 +243,7 @@ describe("Header", () => {
242
243
  );
243
244
 
244
245
  await testSetup.renderOnce();
245
- const frame = testSetup.captureCharFrame();
246
+ const frame = testSetup!.captureCharFrame();
246
247
 
247
248
  expect(frame).toContain("v0.3.0 available");
248
249
  expect(frame).toContain("run npm install -g gloomberb@latest");
@@ -564,6 +565,95 @@ describe("Shell", () => {
564
565
  expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
565
566
  });
566
567
 
568
+ test("updates the floating pane preview before mouse release", async () => {
569
+ const config = createDefaultConfig("/tmp/gloomberb-shell-live-floating-drag-test");
570
+ const floatingLayout = cloneLayout(config.layout);
571
+ floatingLayout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
572
+ floatingLayout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }];
573
+ await renderShellForWindowModeTest(
574
+ createShellStateWithLayout(config, floatingLayout, "ticker-detail:main"),
575
+ { width: 80, height: 18 },
576
+ );
577
+
578
+ await act(async () => {
579
+ await testSetup!.mockMouse.pressDown(10, 3);
580
+ await testSetup!.renderOnce();
581
+ await testSetup!.mockMouse.moveTo(16, 6);
582
+ await testSetup!.renderOnce();
583
+ await testSetup!.renderOnce();
584
+ });
585
+
586
+ const frame = testSetup!.captureCharFrame();
587
+ const rows = frame.split("\n");
588
+ expect(rows[2]?.indexOf("┌─:: Main Portfolio") ?? -1).toBeLessThan(0);
589
+ expect(rows[5]?.indexOf("┌─:: Main Portfolio")).toBeGreaterThanOrEqual(14);
590
+
591
+ await act(async () => {
592
+ await testSetup!.mockMouse.release(16, 6);
593
+ await testSetup!.renderOnce();
594
+ });
595
+ });
596
+
597
+ test("keeps the focused textarea cursor visible when it is not covered", async () => {
598
+ const config = createDefaultConfig("/tmp/gloomberb-shell-cursor-visible-test");
599
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
600
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
601
+ const registry = createShellPluginRegistry({
602
+ portfolioListComponent: ({ focused, width, height }) => (
603
+ <Textarea
604
+ initialValue=""
605
+ focused={focused}
606
+ width={width}
607
+ height={Math.max(1, height)}
608
+ />
609
+ ),
610
+ tickerDetailComponent: () => <text>Chart Body</text>,
611
+ });
612
+ const layout = {
613
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
614
+ instances: [{ ...mainPane }, { ...detailPane }],
615
+ floating: [{ instanceId: "ticker-detail:main", x: 30, y: 2, width: 24, height: 8, zIndex: 75 }],
616
+ detached: [],
617
+ };
618
+ await renderShellForWindowModeTest(
619
+ createShellStateWithLayout(config, layout, "portfolio-list:main"),
620
+ { registry, width: 80, height: 18 },
621
+ );
622
+ await testSetup!.renderOnce();
623
+
624
+ expect(testSetup!.renderer.getCursorState().visible).toBe(true);
625
+ });
626
+
627
+ test("hides the textarea cursor when a higher floating pane covers it", async () => {
628
+ const config = createDefaultConfig("/tmp/gloomberb-shell-cursor-occlusion-test");
629
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
630
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
631
+ const registry = createShellPluginRegistry({
632
+ portfolioListComponent: ({ focused, width, height }) => (
633
+ <Textarea
634
+ initialValue=""
635
+ focused={focused}
636
+ width={width}
637
+ height={Math.max(1, height)}
638
+ />
639
+ ),
640
+ tickerDetailComponent: () => <text>Chart Body</text>,
641
+ });
642
+ const layout = {
643
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
644
+ instances: [{ ...mainPane }, { ...detailPane }],
645
+ floating: [{ instanceId: "ticker-detail:main", x: 0, y: 0, width: 40, height: 8, zIndex: 75 }],
646
+ detached: [],
647
+ };
648
+ await renderShellForWindowModeTest(
649
+ createShellStateWithLayout(config, layout, "portfolio-list:main"),
650
+ { registry, width: 80, height: 18 },
651
+ );
652
+ await testSetup!.renderOnce();
653
+
654
+ expect(testSetup!.renderer.getCursorState().visible).toBe(false);
655
+ });
656
+
567
657
  test("keeps focused pane local detail state when maximizing a floating pane", async () => {
568
658
  const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-local-state-test");
569
659
  const detailPane = requireLayoutInstance(config, "ticker-detail:main");
@@ -1,4 +1,4 @@
1
- import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost } from "../../../ui";
1
+ import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost, type BoxRenderable } from "../../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useRendererHost, useUiCapabilities } from "../../../ui";
4
4
  import { useViewport } from "../../../react/input";
@@ -61,6 +61,10 @@ import {
61
61
  import { useShellPaneActions } from "./pane/actions";
62
62
  import { resolvePaneFocusSourceLayout } from "./fullscreen";
63
63
  import { useTransientLayout } from "../transient-layout";
64
+ import {
65
+ resolveShellCursorOcclusionRects,
66
+ useShellCursorOcclusionGuard,
67
+ } from "./cursor-occlusion";
64
68
 
65
69
  export { resolveAppHeaderHeightCells } from "./chrome";
66
70
  export { buildNativeWindowState } from "./native/window-state";
@@ -102,6 +106,7 @@ export function Shell({
102
106
  const { nativePaneChrome = false, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
103
107
  const { showContextMenu } = useContextMenu();
104
108
  const { width, height } = useViewport();
109
+ const shellRef = useRef<BoxRenderable | null>(null);
105
110
 
106
111
  const appHeaderHeight = resolveAppHeaderHeightCells({ titleBarOverlay, cellHeightPx });
107
112
  const contentHeight = Math.max(1, height - appHeaderHeight - (statusBarVisible ? 1 : 0));
@@ -124,7 +129,11 @@ export function Shell({
124
129
  }, []);
125
130
  const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
126
131
 
127
- const dragRuntime = useShellDragRuntimeState({ contentHeight, width });
132
+ const dragRuntime = useShellDragRuntimeState({
133
+ contentHeight,
134
+ throttleFloatingPreview: nativePaneChrome,
135
+ width,
136
+ });
128
137
  const {
129
138
  cancelActiveDrag,
130
139
  dividerPreview,
@@ -209,6 +218,27 @@ export function Shell({
209
218
  pluginRegistry,
210
219
  width,
211
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
+ });
212
242
 
213
243
  const {
214
244
  closeAllFloatingPanes,
@@ -485,12 +515,24 @@ export function Shell({
485
515
 
486
516
  return (
487
517
  <Box
518
+ ref={shellRef}
488
519
  flexDirection="row"
489
520
  flexGrow={1}
521
+ flexShrink={1}
522
+ flexBasis={0}
523
+ minWidth={0}
524
+ minHeight={0}
490
525
  height={nativePaneChrome ? undefined : contentHeight}
491
526
  position={nativePaneChrome ? "relative" : undefined}
492
527
  overflow="hidden"
493
- {...(!nativePaneChrome ? { onMouse: handleMouse } : {})}
528
+ {...(!nativePaneChrome
529
+ ? {
530
+ onMouseDown: handleMouse,
531
+ onMouseDrag: handleMouse,
532
+ onMouseDragEnd: handleMouse,
533
+ onMouseUp: handleMouse,
534
+ }
535
+ : {})}
494
536
  >
495
537
  <Box
496
538
  position="absolute"
@@ -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 };
@@ -111,11 +111,14 @@ export function ShellPaneLayers({
111
111
  >
112
112
  <PaneFooterProvider>
113
113
  {(footer) => {
114
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
114
+ const showFooter = hasPaneFooterContent(footer);
115
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
116
+ const renderFooter = reserveFooter || showFooter;
115
117
  const bodyFrame = resolvePaneBodyFrame({
116
118
  width: rect.width,
117
119
  height: rect.height,
118
120
  nativePaneChrome,
121
+ footerVisible: renderFooter,
119
122
  reserveFooter,
120
123
  });
121
124
  return (
@@ -129,7 +132,7 @@ export function ShellPaneLayers({
129
132
  windowModeSelected={windowModeSelected}
130
133
  footer={footer}
131
134
  onMouseDown={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
132
- onMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
135
+ onHeaderMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
133
136
  onHeaderMouseDown={nativePaneChrome && !transientFocusActive ? (event) => startNativeDockedDrag(leaf.instanceId, rect, event) : undefined}
134
137
  onHeaderMouseDrag={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
135
138
  onHeaderMouseDragEnd={nativePaneChrome && !transientFocusActive ? handleNativeDrag : undefined}
@@ -165,11 +168,14 @@ export function ShellPaneLayers({
165
168
  return (
166
169
  <PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
167
170
  {(footer) => {
168
- const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
171
+ const showFooter = hasPaneFooterContent(footer);
172
+ const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
173
+ const renderFooter = reserveFooter || showFooter;
169
174
  const bodyFrame = resolvePaneBodyFrame({
170
175
  width: preview.width,
171
176
  height: preview.height,
172
177
  nativePaneChrome,
178
+ footerVisible: renderFooter,
173
179
  reserveFooter,
174
180
  });
175
181
  return (
@@ -186,7 +192,7 @@ export function ShellPaneLayers({
186
192
  showActions={showActions}
187
193
  footer={footer}
188
194
  onMouseDown={nativePaneChrome ? (event) => handleNativePaneMouseDown(pane.instance.instanceId, event) : undefined}
189
- onMouseMove={() => setHoveredPaneIfChanged(pane.instance.instanceId)}
195
+ onHeaderMouseMove={() => setHoveredPaneIfChanged(pane.instance.instanceId)}
190
196
  onHeaderMouseDown={nativePaneChrome ? (event) => startNativeFloatingDrag(pane.instance.instanceId, preview, event) : undefined}
191
197
  onHeaderMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
192
198
  onHeaderMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
@@ -0,0 +1,17 @@
1
+ import {
2
+ removeUnavailablePaneTypes,
3
+ type PaneTypeAvailability,
4
+ } from "../../../plugins/pane-manager";
5
+ import type { LayoutConfig } from "../../../types/config";
6
+
7
+ export function resolveShellVisibleLayout(
8
+ layout: LayoutConfig,
9
+ disabledPaneIds: ReadonlySet<string>,
10
+ registeredPaneIds: PaneTypeAvailability,
11
+ ): LayoutConfig {
12
+ return removeUnavailablePaneTypes(
13
+ layout,
14
+ registeredPaneIds,
15
+ { disabledPaneIds },
16
+ );
17
+ }
@@ -1,7 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { testRender } from "../../renderers/opentui/test-utils";
3
3
  import { AppContext, createInitialState } from "../../state/app/context";
4
- import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types/config";
4
+ import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../types/config";
5
5
  import type { AppNotificationRequest } from "../../types/plugin";
6
6
  import { StatusBar } from "./status-bar";
7
7
  import { setSharedRegistryForTests } from "../../plugins/registry";
@@ -174,6 +174,10 @@ describe("StatusBar", () => {
174
174
  const notifications: AppNotificationRequest[] = [];
175
175
 
176
176
  setSharedRegistryForTests({
177
+ panes: new Map([
178
+ ["portfolio-list", {}],
179
+ [TICKER_RESEARCH_PANE_ID, {}],
180
+ ]),
177
181
  getLayoutFn: () => state.config.layout,
178
182
  getTermSizeFn: () => ({ width: 120, height: 40 }),
179
183
  updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
@@ -233,6 +237,7 @@ describe("StatusBar", () => {
233
237
  globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
234
238
 
235
239
  setSharedRegistryForTests({
240
+ panes: new Map(),
236
241
  getLayoutFn: () => state.config.layout,
237
242
  getTermSizeFn: () => ({ width: 120, height: 40 }),
238
243
  updateLayoutFn: () => {},
@@ -114,7 +114,11 @@ export function StatusBar() {
114
114
  event?.stopPropagation?.();
115
115
  if (!registry) return;
116
116
  const { width, height } = registry.getTermSizeFn();
117
- registry.updateLayoutFn(gridlockAllPanes(registry.getLayoutFn(), { x: 0, y: 0, width, height }));
117
+ registry.updateLayoutFn(gridlockAllPanes(
118
+ registry.getLayoutFn(),
119
+ { x: 0, y: 0, width, height },
120
+ registry.panes,
121
+ ));
118
122
  notifyGridlockComplete(registry.notify.bind(registry), () => {
119
123
  dispatch({ type: "UNDO_LAYOUT" });
120
124
  });
@@ -360,7 +364,7 @@ function CommandBarHint({
360
364
  <Text
361
365
  fg={hovered ? colors.text : colors.textDim}
362
366
  {...(!nativePaneChrome ? { bg: hovered ? hoverBg() : undefined } : {})}
363
- onMouseMove={() => setHoveredControl((current) => (current === "command-bar" ? current : "command-bar"))}
367
+ onMouseOver={() => setHoveredControl((current) => (current === "command-bar" ? current : "command-bar"))}
364
368
  onMouseDown={openCommandBar}
365
369
  {...(nativePaneChrome ? { "data-gloom-interactive": "true" } : {})}
366
370
  >
@@ -381,7 +385,7 @@ function NativeGridlockTip({
381
385
  <Text
382
386
  fg={hoveredControl === "gridlock-tip" ? colors.textBright : colors.borderFocused}
383
387
  attributes={TextAttributes.BOLD}
384
- onMouseMove={() => setHoveredControl((current) => (current === "gridlock-tip" ? current : "gridlock-tip"))}
388
+ onMouseOver={() => setHoveredControl((current) => (current === "gridlock-tip" ? current : "gridlock-tip"))}
385
389
  onMouseDown={handleGridlockTip}
386
390
  data-gloom-interactive="true"
387
391
  >
@@ -389,7 +393,7 @@ function NativeGridlockTip({
389
393
  </Text>
390
394
  <Text
391
395
  fg={hoveredControl === "gridlock-tip-dismiss" ? colors.text : colors.textDim}
392
- onMouseMove={() => setHoveredControl((current) => (current === "gridlock-tip-dismiss" ? current : "gridlock-tip-dismiss"))}
396
+ onMouseOver={() => setHoveredControl((current) => (current === "gridlock-tip-dismiss" ? current : "gridlock-tip-dismiss"))}
393
397
  onMouseDown={dismissGridlockTip}
394
398
  data-gloom-interactive="true"
395
399
  >
@@ -411,14 +415,14 @@ function TerminalGridlockTip({
411
415
  <Box width={1} />
412
416
  <Box
413
417
  backgroundColor={hoveredControl === "gridlock-tip" ? hoverBg() : colors.header}
414
- onMouseMove={() => setHoveredControl((current) => (current === "gridlock-tip" ? current : "gridlock-tip"))}
418
+ onMouseOver={() => setHoveredControl((current) => (current === "gridlock-tip" ? current : "gridlock-tip"))}
415
419
  onMouseDown={handleGridlockTip}
416
420
  >
417
421
  <Text fg={colors.headerText}> Gridlock All </Text>
418
422
  </Box>
419
423
  <Text
420
424
  fg={hoveredControl === "gridlock-tip-dismiss" ? colors.text : colors.textDim}
421
- onMouseMove={() => setHoveredControl((current) => (current === "gridlock-tip-dismiss" ? current : "gridlock-tip-dismiss"))}
425
+ onMouseOver={() => setHoveredControl((current) => (current === "gridlock-tip-dismiss" ? current : "gridlock-tip-dismiss"))}
422
426
  onMouseDown={dismissGridlockTip}
423
427
  >
424
428
  {" x"}