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
@@ -2,11 +2,18 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { TestDialogProvider, testRender } from "../../../renderers/opentui/test-utils";
3
3
  import { openTuiUiHost } from "../../../renderers/opentui/ui-host";
4
4
  import type { ReactNode } from "react";
5
- import { act, useReducer } from "react";
6
- import { AppContext, appReducer, createInitialState } from "../../../state/app/context";
5
+ import { act, useReducer, useState } from "react";
6
+ import {
7
+ AppContext,
8
+ appReducer,
9
+ createInitialState,
10
+ resolveTickerForPane,
11
+ usePaneTicker,
12
+ } from "../../../state/app/context";
7
13
  import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../../types/config";
8
14
  import type { PluginRegistry } from "../../../plugins/registry";
9
15
  import type { PaneProps } from "../../../types/plugin";
16
+ import { Textarea } from "../../../ui";
10
17
  import { Header } from "../header";
11
18
  import {
12
19
  buildNativeWindowState,
@@ -14,6 +21,8 @@ import {
14
21
  resolveAppHeaderHeightCells,
15
22
  Shell,
16
23
  } from "./index";
24
+ import { resolvePaneFocusSourceLayout } from "./fullscreen";
25
+ import { TransientLayoutProvider, useTransientLayout, type TransientLayoutState } from "../transient-layout";
17
26
 
18
27
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
19
28
 
@@ -133,6 +142,44 @@ async function renderShellForWindowModeTest(
133
142
  return { actions, registry };
134
143
  }
135
144
 
145
+ function CaptureTransientLayout({
146
+ controls,
147
+ }: {
148
+ controls: { transientLayout: TransientLayoutState | null };
149
+ }) {
150
+ const { transientLayout } = useTransientLayout();
151
+ controls.transientLayout = transientLayout;
152
+ return null;
153
+ }
154
+
155
+ function ShellTransientHarness({
156
+ initialState,
157
+ registry,
158
+ controls,
159
+ }: {
160
+ initialState: ReturnType<typeof createInitialState>;
161
+ registry: PluginRegistry;
162
+ controls: {
163
+ dispatch?: (action: ShellTestAction) => void;
164
+ state?: ReturnType<typeof createInitialState>;
165
+ transientLayout: TransientLayoutState | null;
166
+ };
167
+ }) {
168
+ const [state, dispatch] = useReducer(appReducer, initialState);
169
+ controls.dispatch = dispatch;
170
+ controls.state = state;
171
+ return (
172
+ <AppContext value={{ state, dispatch }}>
173
+ <TransientLayoutProvider>
174
+ <TestDialogProvider>
175
+ <Shell pluginRegistry={registry} />
176
+ </TestDialogProvider>
177
+ <CaptureTransientLayout controls={controls} />
178
+ </TransientLayoutProvider>
179
+ </AppContext>
180
+ );
181
+ }
182
+
136
183
  function findUpdateLayout(actions: ShellTestAction[]) {
137
184
  return actions.find((action) => action.type === "UPDATE_LAYOUT");
138
185
  }
@@ -176,7 +223,7 @@ describe("Header", () => {
176
223
  );
177
224
 
178
225
  await testSetup.renderOnce();
179
- const frame = testSetup.captureCharFrame();
226
+ const frame = testSetup!.captureCharFrame();
180
227
 
181
228
  expect(frame).toContain("v0.3.0 available");
182
229
  expect(frame).toContain("starting download");
@@ -196,7 +243,7 @@ describe("Header", () => {
196
243
  );
197
244
 
198
245
  await testSetup.renderOnce();
199
- const frame = testSetup.captureCharFrame();
246
+ const frame = testSetup!.captureCharFrame();
200
247
 
201
248
  expect(frame).toContain("v0.3.0 available");
202
249
  expect(frame).toContain("run npm install -g gloomberb@latest");
@@ -417,6 +464,7 @@ describe("Shell", () => {
417
464
  expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
418
465
  expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
419
466
  expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
467
+ expect(resolvePaneManagementShortcut({ ...base, name: "f", key: "f" })).toBe("toggle-fullscreen");
420
468
  expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
421
469
  expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode");
422
470
  expect(resolvePaneManagementShortcut({ ...base, name: "r", key: "r" })).toBe("window-resize-mode");
@@ -425,6 +473,441 @@ describe("Shell", () => {
425
473
  expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
426
474
  });
427
475
 
476
+ test("toggles the focused pane fullscreen without persisting layout", async () => {
477
+ const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-shortcut-test");
478
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
479
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
480
+ const dockedLayout = {
481
+ dockRoot: {
482
+ kind: "split" as const,
483
+ axis: "horizontal" as const,
484
+ ratio: 0.5,
485
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
486
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
487
+ },
488
+ instances: [{ ...mainPane }, { ...detailPane }],
489
+ floating: [],
490
+ detached: [],
491
+ };
492
+ const { actions } = await renderShellForWindowModeTest(
493
+ createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
494
+ { width: 80, height: 18 },
495
+ );
496
+
497
+ expect(testSetup.captureCharFrame()).toContain("Main Portfolio");
498
+ expect(testSetup.captureCharFrame()).toContain("Ticker Research Body");
499
+
500
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
501
+ let frame = testSetup.captureCharFrame();
502
+ expect(frame).toContain("Main Portfolio");
503
+ expect(frame).not.toContain("Ticker Research Body");
504
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
505
+
506
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
507
+ await act(async () => {
508
+ await testSetup!.renderOnce();
509
+ });
510
+ frame = testSetup.captureCharFrame();
511
+ expect(frame).toContain("Main Portfolio");
512
+ expect(frame).toContain("Ticker Research Body");
513
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
514
+ });
515
+
516
+ test("captures the source layout for transient pane focus", () => {
517
+ const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-layout-test");
518
+ const layout = cloneLayout(config.layout);
519
+ layout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
520
+ layout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }];
521
+
522
+ const focusedLayout = resolvePaneFocusSourceLayout(layout, "ticker-detail:main");
523
+
524
+ expect(focusedLayout).toMatchObject({
525
+ dockRoot: { kind: "pane", instanceId: "portfolio-list:main" },
526
+ floating: [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }],
527
+ });
528
+ expect(focusedLayout).not.toBe(layout);
529
+ expect(focusedLayout?.instances.map((instance) => instance.instanceId)).toEqual(
530
+ layout.instances.map((instance) => instance.instanceId),
531
+ );
532
+ });
533
+
534
+ test("locks layout-changing mouse drags while fullscreen is active", async () => {
535
+ const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-drag-test");
536
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
537
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
538
+ const dockedLayout = {
539
+ dockRoot: {
540
+ kind: "split" as const,
541
+ axis: "horizontal" as const,
542
+ ratio: 0.5,
543
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
544
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
545
+ },
546
+ instances: [{ ...mainPane }, { ...detailPane }],
547
+ floating: [],
548
+ detached: [],
549
+ };
550
+ const { actions } = await renderShellForWindowModeTest(
551
+ createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
552
+ { width: 80, height: 18 },
553
+ );
554
+
555
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
556
+ await act(async () => {
557
+ await testSetup!.mockMouse.drag(4, 0, 32, 4);
558
+ await testSetup!.renderOnce();
559
+ await testSetup!.renderOnce();
560
+ });
561
+
562
+ const frame = testSetup.captureCharFrame();
563
+ expect(frame).toContain("Main Portfolio");
564
+ expect(frame).not.toContain("Ticker Research Body");
565
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
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
+
657
+ test("keeps focused pane local detail state when maximizing a floating pane", async () => {
658
+ const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-local-state-test");
659
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
660
+ let openDetail: (() => void) | null = null;
661
+ const registry = createShellPluginRegistry({
662
+ tickerDetailComponent: () => {
663
+ const [detailOpen, setDetailOpen] = useState(false);
664
+ openDetail = () => setDetailOpen(true);
665
+ return <text>{detailOpen ? "Detail State" : "Table State"}</text>;
666
+ },
667
+ });
668
+ const floatingLayout = {
669
+ dockRoot: null,
670
+ instances: [{ ...detailPane }],
671
+ floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 40, height: 10, zIndex: 50 }],
672
+ detached: [],
673
+ };
674
+ await renderShellForWindowModeTest(
675
+ createShellStateWithLayout(config, floatingLayout, "ticker-detail:main"),
676
+ { registry, width: 80, height: 18 },
677
+ );
678
+
679
+ await act(async () => {
680
+ openDetail?.();
681
+ await testSetup!.renderOnce();
682
+ });
683
+ expect(testSetup.captureCharFrame()).toContain("etail State");
684
+
685
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
686
+
687
+ const frame = testSetup.captureCharFrame();
688
+ expect(frame).toContain("etail State");
689
+ expect(frame).not.toContain("able State");
690
+ });
691
+
692
+ test("allows pane fullscreen shortcut while text input is captured on desktop", async () => {
693
+ const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-captured-input-test");
694
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
695
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
696
+ const dockedLayout = {
697
+ dockRoot: {
698
+ kind: "split" as const,
699
+ axis: "horizontal" as const,
700
+ ratio: 0.5,
701
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
702
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
703
+ },
704
+ instances: [{ ...mainPane }, { ...detailPane }],
705
+ floating: [],
706
+ detached: [],
707
+ };
708
+ const { actions } = await renderShellForWindowModeTest(
709
+ {
710
+ ...createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
711
+ inputCaptured: true,
712
+ },
713
+ { width: 80, height: 18 },
714
+ );
715
+
716
+ await emitKeypress({
717
+ name: "f",
718
+ key: "f",
719
+ meta: true,
720
+ super: true,
721
+ shift: true,
722
+ targetEditable: true,
723
+ } as any);
724
+
725
+ const frame = testSetup.captureCharFrame();
726
+ expect(frame).toContain("Main Portfolio");
727
+ expect(frame).not.toContain("Ticker Research Body");
728
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
729
+ });
730
+
731
+ test("keeps the transient focus layout available after switching saved layouts", async () => {
732
+ const config = createDefaultConfig("/tmp/gloomberb-shell-transient-focus-layout-tab-test");
733
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
734
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
735
+ const defaultLayout = {
736
+ dockRoot: {
737
+ kind: "split" as const,
738
+ axis: "horizontal" as const,
739
+ ratio: 0.5,
740
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
741
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
742
+ },
743
+ instances: [{ ...mainPane }, { ...detailPane }],
744
+ floating: [],
745
+ detached: [],
746
+ };
747
+ const monitorLayout = {
748
+ dockRoot: { kind: "pane" as const, instanceId: "ticker-detail:main" },
749
+ instances: [{ ...detailPane }],
750
+ floating: [],
751
+ detached: [],
752
+ };
753
+ config.layout = cloneLayout(defaultLayout);
754
+ config.layouts = [
755
+ { name: "Default", layout: cloneLayout(defaultLayout), focusedPaneId: "portfolio-list:main" },
756
+ { name: "Monitor", layout: cloneLayout(monitorLayout), focusedPaneId: "ticker-detail:main" },
757
+ ];
758
+ config.activeLayoutIndex = 0;
759
+
760
+ const initialState = {
761
+ ...createInitialState(config),
762
+ focusedPaneId: "portfolio-list:main",
763
+ statusBarVisible: true,
764
+ };
765
+ const registry = createShellPluginRegistry();
766
+ const controls: {
767
+ dispatch?: (action: ShellTestAction) => void;
768
+ state?: ReturnType<typeof createInitialState>;
769
+ transientLayout: TransientLayoutState | null;
770
+ } = { transientLayout: null };
771
+
772
+ testSetup = await testRender(
773
+ <ShellTransientHarness initialState={initialState} registry={registry} controls={controls} />,
774
+ { width: 100, height: 18 },
775
+ );
776
+
777
+ await testSetup.renderOnce();
778
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
779
+
780
+ let frame = testSetup.captureCharFrame();
781
+ expect(frame).toContain("Main Portfolio");
782
+ expect(frame).not.toContain("Ticker Research Body");
783
+ expect(controls.transientLayout).toMatchObject({
784
+ id: "pane-focus",
785
+ active: true,
786
+ });
787
+
788
+ await act(async () => {
789
+ controls.transientLayout?.onDeactivate?.();
790
+ controls.dispatch?.({ type: "SWITCH_LAYOUT", index: 1 });
791
+ await testSetup!.renderOnce();
792
+ await testSetup!.renderOnce();
793
+ });
794
+
795
+ expect(controls.transientLayout).toMatchObject({
796
+ id: "pane-focus",
797
+ active: false,
798
+ });
799
+
800
+ await act(async () => {
801
+ controls.transientLayout?.onActivate?.();
802
+ await testSetup!.renderOnce();
803
+ await testSetup!.renderOnce();
804
+ await testSetup!.renderOnce();
805
+ await testSetup!.renderOnce();
806
+ });
807
+
808
+ expect(controls.transientLayout).toMatchObject({
809
+ id: "pane-focus",
810
+ active: true,
811
+ });
812
+ });
813
+
814
+ test("restores the focus source layout state when reactivating transient focus", async () => {
815
+ const config = createDefaultConfig("/tmp/gloomberb-shell-transient-focus-state-test");
816
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
817
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
818
+ const defaultLayout = {
819
+ dockRoot: {
820
+ kind: "split" as const,
821
+ axis: "horizontal" as const,
822
+ ratio: 0.5,
823
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
824
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
825
+ },
826
+ instances: [{ ...mainPane }, { ...detailPane }],
827
+ floating: [],
828
+ detached: [],
829
+ };
830
+ const monitorLayout = cloneLayout(defaultLayout);
831
+ config.layout = cloneLayout(defaultLayout);
832
+ config.layouts = [
833
+ {
834
+ name: "Default",
835
+ layout: cloneLayout(defaultLayout),
836
+ focusedPaneId: "ticker-detail:main",
837
+ paneState: {
838
+ "portfolio-list:main": { collectionId: "main", cursorSymbol: "MSTR" },
839
+ "ticker-detail:main": { activeTabId: "overview" },
840
+ },
841
+ },
842
+ {
843
+ name: "Monitor",
844
+ layout: monitorLayout,
845
+ focusedPaneId: "ticker-detail:main",
846
+ paneState: {
847
+ "portfolio-list:main": { collectionId: "main", cursorSymbol: null },
848
+ "ticker-detail:main": { activeTabId: "overview" },
849
+ },
850
+ },
851
+ ];
852
+ config.activeLayoutIndex = 0;
853
+ const registry = createShellPluginRegistry({
854
+ tickerDetailComponent: () => {
855
+ const { symbol } = usePaneTicker();
856
+ return <text>{`Ticker:${symbol ?? "none"}`}</text>;
857
+ },
858
+ });
859
+ const controls: {
860
+ dispatch?: (action: ShellTestAction) => void;
861
+ state?: ReturnType<typeof createInitialState>;
862
+ transientLayout: TransientLayoutState | null;
863
+ } = { transientLayout: null };
864
+
865
+ testSetup = await testRender(
866
+ <ShellTransientHarness
867
+ initialState={createInitialState(config)}
868
+ registry={registry}
869
+ controls={controls}
870
+ />,
871
+ { width: 100, height: 18 },
872
+ );
873
+
874
+ await testSetup.renderOnce();
875
+ expect(testSetup.captureCharFrame()).toContain("icker:MSTR");
876
+
877
+ await emitKeypress({ name: "f", ctrl: true, shift: true });
878
+ expect(testSetup.captureCharFrame()).toContain("icker:MSTR");
879
+
880
+ await act(async () => {
881
+ controls.transientLayout?.onDeactivate?.();
882
+ controls.dispatch?.({ type: "SWITCH_LAYOUT", index: 1 });
883
+ await testSetup!.renderOnce();
884
+ await testSetup!.renderOnce();
885
+ });
886
+ expect(controls.transientLayout).toMatchObject({
887
+ id: "pane-focus",
888
+ active: false,
889
+ });
890
+ expect(controls.state?.config.activeLayoutIndex).toBe(1);
891
+ expect(controls.state?.paneState["portfolio-list:main"]?.cursorSymbol).toBeNull();
892
+ expect(controls.state ? resolveTickerForPane(controls.state, "ticker-detail:main") : null).toBeNull();
893
+
894
+ await act(async () => {
895
+ controls.transientLayout?.onActivate?.();
896
+ await testSetup!.renderOnce();
897
+ await testSetup!.renderOnce();
898
+ await testSetup!.renderOnce();
899
+ await testSetup!.renderOnce();
900
+ });
901
+
902
+ expect(controls.transientLayout).toMatchObject({
903
+ id: "pane-focus",
904
+ active: true,
905
+ });
906
+ expect(controls.state?.config.activeLayoutIndex).toBe(0);
907
+ expect(controls.state?.paneState["portfolio-list:main"]?.cursorSymbol).toBe("MSTR");
908
+ expect(controls.state ? resolveTickerForPane(controls.state, "ticker-detail:main") : null).toBe("MSTR");
909
+ });
910
+
428
911
  test("exits window mode on Enter without changes", async () => {
429
912
  const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
430
913
  const mainPane = requireLayoutInstance(config, "portfolio-list:main");