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
@@ -67,10 +67,10 @@ interface UseStockChartViewportRuntimeOptions {
67
67
  persistChartControls: (range: TimeRange, resolution: ChartResolution) => void;
68
68
  renderedResolution: ChartResolution;
69
69
  selectionSupportMap: ReadonlyMap<ManualChartResolution, TimeRange>;
70
+ persistChartRenderMode: (mode: ChartRenderMode) => void;
70
71
  setPendingAutoWindowOverride: Dispatch<SetStateAction<DateWindowRange | null>>;
71
72
  setRenderedAutoView: Dispatch<SetStateAction<AutoRenderedView | null>>;
72
73
  setRequestedResolution: Dispatch<SetStateAction<ChartResolution>>;
73
- setStoredRenderMode: Dispatch<SetStateAction<ChartRenderMode>>;
74
74
  setViewState: Dispatch<SetStateAction<StockChartViewportState>>;
75
75
  storedRangePreset: TimeRange;
76
76
  storedRenderMode: ChartRenderMode;
@@ -101,12 +101,12 @@ export function useStockChartViewportRuntime({
101
101
  pendingCanonicalResetRef,
102
102
  pendingExpansionRef,
103
103
  persistChartControls,
104
+ persistChartRenderMode,
104
105
  renderedResolution,
105
106
  selectionSupportMap,
106
107
  setPendingAutoWindowOverride,
107
108
  setRenderedAutoView,
108
109
  setRequestedResolution,
109
- setStoredRenderMode,
110
110
  setViewState,
111
111
  storedRangePreset,
112
112
  storedRenderMode,
@@ -172,10 +172,10 @@ export function useStockChartViewportRuntime({
172
172
  });
173
173
 
174
174
  const persistRenderMode = useCallback((nextMode: ChartRenderMode) => {
175
- if (!compact && nextMode !== storedRenderMode) {
176
- setStoredRenderMode(nextMode);
175
+ if (!compact) {
176
+ persistChartRenderMode(nextMode);
177
177
  }
178
- }, [compact, setStoredRenderMode, storedRenderMode]);
178
+ }, [compact, persistChartRenderMode]);
179
179
 
180
180
  const expandBufferRange = useCallback((action: PendingExpansionAction): boolean => {
181
181
  if (compact) return false;
@@ -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
  },
@@ -16,7 +16,17 @@ import {
16
16
  getPaneTemplateDisplayLabel,
17
17
  } from "./items";
18
18
 
19
+ function paneTemplateHasConfigFields(template: PaneTemplateDef): boolean {
20
+ if (template.wizard?.some((step) => step.type !== "info")) {
21
+ return true;
22
+ }
23
+ return !!buildGeneratedTemplateField(template, null).field;
24
+ }
25
+
19
26
  export function shouldOpenPaneTemplateConfig(template: PaneTemplateDef, arg?: string): boolean {
27
+ if (!paneTemplateHasConfigFields(template)) {
28
+ return false;
29
+ }
20
30
  if (template.wizard && template.wizard.length > 0) {
21
31
  if (!arg?.trim()) {
22
32
  return true;
@@ -117,6 +117,42 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
117
117
  });
118
118
  }
119
119
 
120
+ function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
121
+ mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
122
+ id: "prediction-markets",
123
+ name: "Prediction Markets",
124
+ component: () => null,
125
+ defaultPosition: "left",
126
+ defaultMode: "floating",
127
+ });
128
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("new-prediction-markets-pane", {
129
+ id: "new-prediction-markets-pane",
130
+ paneId: "prediction-markets",
131
+ label: "Prediction Markets",
132
+ description: "Open a new prediction markets browser pane",
133
+ keywords: ["prediction", "markets", "polymarket", "kalshi", "events"],
134
+ shortcut: { prefix: "PM", argPlaceholder: "query", argKind: "text" },
135
+ });
136
+ }
137
+
138
+ function registerShortcutOnlyPane(pluginRegistry: MutablePaneRegistry): void {
139
+ mutableRegistryMap(pluginRegistry.panes).set("top-news", {
140
+ id: "top-news",
141
+ name: "Top News",
142
+ component: () => null,
143
+ defaultPosition: "right",
144
+ defaultMode: "floating",
145
+ });
146
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("top-news-pane", {
147
+ id: "top-news-pane",
148
+ paneId: "top-news",
149
+ label: "Top News",
150
+ description: "Curated top market stories ranked by importance",
151
+ keywords: ["top", "news", "headlines", "stories"],
152
+ shortcut: { prefix: "TOP" },
153
+ });
154
+ }
155
+
120
156
  describe("CommandBar pane and layout routes", () => {
121
157
  const layoutModeConfig = (config: AppConfig): AppConfig => {
122
158
  const research = cloneLayout(config.layout);
@@ -271,6 +307,46 @@ describe("CommandBar pane and layout routes", () => {
271
307
  expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
272
308
  });
273
309
 
310
+ for (const scenario of [
311
+ {
312
+ query: "prediction markets",
313
+ register: registerQueryOnlyPane,
314
+ templateId: "new-prediction-markets-pane",
315
+ },
316
+ {
317
+ query: "top news",
318
+ register: registerShortcutOnlyPane,
319
+ templateId: "top-news-pane",
320
+ },
321
+ ] as const) {
322
+ test(`creates ${scenario.templateId} directly when it has no config fields`, async () => {
323
+ const created: CreatedPaneCall[] = [];
324
+
325
+ testSetup = await testRender(<CommandBarHarness
326
+ query={scenario.query}
327
+ live
328
+ configurePluginRegistry={(pluginRegistry) => {
329
+ scenario.register(pluginRegistry);
330
+ recordPaneCreations(pluginRegistry, created);
331
+ }}
332
+ />, {
333
+ width: 100,
334
+ height: 18,
335
+ });
336
+
337
+ await testSetup.renderOnce();
338
+
339
+ await act(async () => {
340
+ testSetup!.mockInput.pressEnter();
341
+ await Bun.sleep(0);
342
+ await testSetup!.renderOnce();
343
+ });
344
+
345
+ expect(created).toEqual([{ templateId: scenario.templateId, options: undefined }]);
346
+ expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
347
+ });
348
+ }
349
+
274
350
  test("shows pane templates that share the same shortcut", async () => {
275
351
  testSetup = await testRender(<CommandBarHarness
276
352
  query="DUP"
@@ -26,6 +26,11 @@ export type DataTableKeyEvent = TableViewKeyEvent;
26
26
 
27
27
  const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
28
28
 
29
+ export interface DataTableRootKeyContext {
30
+ selectedIndex: number;
31
+ itemCount: number;
32
+ }
33
+
29
34
  export type DataTableSelectionChangeReason =
30
35
  | "keyboard"
31
36
  | "pointer"
@@ -95,7 +100,10 @@ export interface DataTableViewProps<
95
100
  hoveredIdx?: number | null;
96
101
  setHoveredIdx?: (index: number | null) => void;
97
102
  keyboardNavigation?: boolean;
98
- onRootKeyDown?: (event: DataTableKeyEvent) => boolean | void;
103
+ onRootKeyDown?: (
104
+ event: DataTableKeyEvent,
105
+ context: DataTableRootKeyContext,
106
+ ) => boolean | void;
99
107
  resetScrollKey?: unknown;
100
108
  }
101
109
 
@@ -486,7 +494,10 @@ export function DataTableView<
486
494
  if (event.defaultPrevented || event.propagationStopped) return;
487
495
  if (!focused || !keyboardNavigation) return;
488
496
 
489
- if (onRootKeyDown?.(event)) return;
497
+ if (onRootKeyDown?.(event, {
498
+ selectedIndex: effectiveSelectedIndexRef.current,
499
+ itemCount: tableProps.items.length,
500
+ })) return;
490
501
  if (tableProps.items.length === 0) return;
491
502
 
492
503
  if (isNextTableRowKey(event)) {
@@ -6,6 +6,7 @@ import { formatTimeAgo } from "../../utils/format";
6
6
  import { isPlainKey } from "../../utils/keyboard";
7
7
  import { toTimestampMillis } from "../../utils/timestamp";
8
8
  import { DataTableStackView } from "../data-table/stack-view";
9
+ import type { DataTableRootKeyContext } from "../data-table/view";
9
10
  import {
10
11
  activeStackIndex,
11
12
  sortIndexedStackRows,
@@ -45,9 +46,16 @@ interface FeedDataTableStackViewProps {
45
46
  rootAfter?: ReactNode;
46
47
  onRootKeyDown?: (event: {
47
48
  name?: string;
49
+ sequence?: string;
50
+ ctrl?: boolean;
51
+ meta?: boolean;
52
+ super?: boolean;
53
+ alt?: boolean;
54
+ option?: boolean;
55
+ shift?: boolean;
48
56
  preventDefault?: () => void;
49
57
  stopPropagation?: () => void;
50
- }) => boolean | void;
58
+ }, context: DataTableRootKeyContext) => boolean | void;
51
59
  sourceLabel?: string;
52
60
  titleLabel?: string;
53
61
  emptyStateTitle?: string;
@@ -8,7 +8,7 @@ export type { TickerListVisibleRange } from "./ticker/list-table-view";
8
8
  export { TickerBadgeList } from "./ticker/badge/list";
9
9
  export { InputSearchBar } from "./input-search-bar";
10
10
  export { DataTableView } from "./data-table/view";
11
- export type { DataTableKeyEvent } from "./data-table/view";
11
+ export type { DataTableKeyEvent, DataTableRootKeyContext } from "./data-table/view";
12
12
  export { DataTableStackView } from "./data-table/stack-view";
13
13
  export { FeedDataTableStackView } from "./feed-data-table/stack-view";
14
14
  export type { FeedDataTableItem } from "./feed-data-table/stack-view";
@@ -0,0 +1,103 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useCallback, useRef, useState, type Dispatch, type SetStateAction } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import { AppContext, createInitialState, type AppAction } from "../state/app/context";
5
+ import { createDefaultConfig } from "../types/config";
6
+ import type { InputRenderable } from "../ui";
7
+ import { InputSearchBar } from "./input-search-bar";
8
+
9
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
+ let setSearchActive: Dispatch<SetStateAction<boolean>> | null = null;
11
+
12
+ afterEach(async () => {
13
+ setSearchActive = null;
14
+ if (!testSetup) return;
15
+ await act(async () => {
16
+ testSetup!.renderer.destroy();
17
+ });
18
+ testSetup = undefined;
19
+ });
20
+
21
+ function Harness({
22
+ actions,
23
+ onNavigateDown,
24
+ }: {
25
+ actions: AppAction[];
26
+ onNavigateDown?: () => void;
27
+ }) {
28
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-input-search-bar"));
29
+ const inputRef = useRef<InputRenderable | null>(null);
30
+ const [active, setActive] = useState(true);
31
+ setSearchActive = setActive;
32
+ const dispatch = useCallback((action: AppAction) => {
33
+ actions.push(action);
34
+ }, [actions]);
35
+
36
+ return (
37
+ <AppContext value={{ state, dispatch }}>
38
+ <InputSearchBar
39
+ value=""
40
+ focused
41
+ active={active}
42
+ width={30}
43
+ focusToken={0}
44
+ inputRef={inputRef}
45
+ placeholder="search"
46
+ debounceMs={100}
47
+ onNavigateDown={onNavigateDown}
48
+ onFocus={() => {}}
49
+ onBlur={() => {}}
50
+ onQueryChange={() => {}}
51
+ />
52
+ </AppContext>
53
+ );
54
+ }
55
+
56
+ describe("InputSearchBar", () => {
57
+ test("captures app input while the search input is active", async () => {
58
+ const actions: AppAction[] = [];
59
+
60
+ testSetup = await testRender(<Harness actions={actions} />, { width: 40, height: 4 });
61
+ await act(async () => {
62
+ await testSetup!.renderOnce();
63
+ });
64
+
65
+ expect(actions).toEqual([{ type: "SET_INPUT_CAPTURED", captured: true }]);
66
+ if (!setSearchActive) throw new Error("search setter was not registered");
67
+
68
+ await act(async () => {
69
+ setSearchActive(false);
70
+ await testSetup!.renderOnce();
71
+ });
72
+
73
+ expect(actions).toEqual([
74
+ { type: "SET_INPUT_CAPTURED", captured: true },
75
+ { type: "SET_INPUT_CAPTURED", captured: false },
76
+ ]);
77
+ });
78
+
79
+ test("moves from the active search input to the table with Down", async () => {
80
+ const actions: AppAction[] = [];
81
+ let navigatedDown = 0;
82
+
83
+ testSetup = await testRender(
84
+ <Harness
85
+ actions={actions}
86
+ onNavigateDown={() => {
87
+ navigatedDown += 1;
88
+ }}
89
+ />,
90
+ { width: 40, height: 4 },
91
+ );
92
+ await act(async () => {
93
+ await testSetup!.renderOnce();
94
+ });
95
+
96
+ await act(async () => {
97
+ testSetup!.mockInput.pressArrow("down");
98
+ await testSetup!.renderOnce();
99
+ });
100
+
101
+ expect(navigatedDown).toBe(1);
102
+ });
103
+ });
@@ -1,6 +1,9 @@
1
1
  import { useCallback, useEffect, useState, type RefObject } from "react";
2
2
  import { Box, Input, Text, type InputRenderable } from "../ui";
3
3
  import { colors } from "../theme/colors";
4
+ import { useAppInputCapture } from "../state/app/input-capture";
5
+ import { useShortcut } from "../react/input";
6
+ import { isPlainArrowDown, stopSearchFocusNavigation } from "../utils/search-focus-navigation";
4
7
 
5
8
  export function InputSearchBar({
6
9
  value,
@@ -12,6 +15,7 @@ export function InputSearchBar({
12
15
  placeholder,
13
16
  debounceMs,
14
17
  normalizeValue = identity,
18
+ onNavigateDown,
15
19
  onFocus,
16
20
  onBlur,
17
21
  onQueryChange,
@@ -25,11 +29,23 @@ export function InputSearchBar({
25
29
  placeholder: string;
26
30
  debounceMs: number;
27
31
  normalizeValue?: (value: string) => string;
32
+ onNavigateDown?: () => void;
28
33
  onFocus: () => void;
29
34
  onBlur: () => void;
30
35
  onQueryChange: (query: string) => void;
31
36
  }) {
32
37
  const [draft, setDraft] = useState(value);
38
+ useAppInputCapture(focused && active);
39
+
40
+ useShortcut((event) => {
41
+ if (!onNavigateDown || !isPlainArrowDown(event)) return;
42
+ stopSearchFocusNavigation(event);
43
+ onNavigateDown();
44
+ }, {
45
+ allowEditable: true,
46
+ enabled: focused && active && !!onNavigateDown,
47
+ phase: "before",
48
+ });
33
49
 
34
50
  useEffect(() => {
35
51
  setDraft(value);
@@ -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
  }}
@@ -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
@@ -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;
@@ -29,6 +29,18 @@ function truncateTitle(title: string, maxWidth: number): string {
29
29
  return `${title.slice(0, maxWidth - 2)}..`;
30
30
  }
31
31
 
32
+ function captureTerminalPointerDrag(renderer: unknown, renderable: unknown): void {
33
+ if (!renderable) return;
34
+ const hostCapture = (renderer as { captureMouseRenderable?: (target: unknown) => void }).captureMouseRenderable;
35
+ if (typeof hostCapture === "function") {
36
+ hostCapture.call(renderer, renderable);
37
+ return;
38
+ }
39
+ const capture = (renderer as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
40
+ if (typeof capture !== "function") return;
41
+ capture.call(renderer, renderable);
42
+ }
43
+
32
44
  function DesktopPaneButton({
33
45
  icon,
34
46
  onMouseDown,
@@ -107,11 +119,17 @@ export function PaneHeader({
107
119
  onCloseMouseDown,
108
120
  }: PaneHeaderProps) {
109
121
  const { nativePaneChrome } = useUiCapabilities();
122
+ const nativeRenderer = useNativeRenderer();
123
+ const terminalHeaderRef = useRef<unknown>(null);
110
124
  const visuallyFocused = focused || windowModeSelected;
111
125
  const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
112
126
  const actionText = showActions ? PANE_HEADER_ACTION : " ";
113
127
  const closeText = floating ? PANE_HEADER_CLOSE : "";
114
128
  const textColor = paneTitleText(visuallyFocused, floating);
129
+ const handleTerminalHeaderMouseDown = useCallback((event: any) => {
130
+ captureTerminalPointerDrag(nativeRenderer, terminalHeaderRef.current);
131
+ onHeaderMouseDown?.(event);
132
+ }, [nativeRenderer, onHeaderMouseDown]);
115
133
 
116
134
  if (nativePaneChrome) {
117
135
  return (
@@ -201,7 +219,16 @@ export function PaneHeader({
201
219
  const fill = "─".repeat(fillLen);
202
220
 
203
221
  return (
204
- <Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
222
+ <Box
223
+ ref={terminalHeaderRef}
224
+ height={PANE_HEADER_HEIGHT}
225
+ width={width}
226
+ backgroundColor={backgroundColor}
227
+ flexDirection="row"
228
+ onMouseDown={handleTerminalHeaderMouseDown}
229
+ onMouseDrag={onHeaderMouseDrag}
230
+ onMouseDragEnd={onHeaderMouseDragEnd}
231
+ >
205
232
  <Text fg={borderColor} selectable={false}>{"┌─"}</Text>
206
233
  <Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
207
234
  <Text fg={borderColor} selectable={false}>{fill}</Text>
@@ -229,7 +256,16 @@ export function PaneHeader({
229
256
  const padding = " ".repeat(Math.max(0, titleWidth - clippedTitle.length));
230
257
 
231
258
  return (
232
- <Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
259
+ <Box
260
+ ref={terminalHeaderRef}
261
+ height={PANE_HEADER_HEIGHT}
262
+ width={width}
263
+ backgroundColor={backgroundColor}
264
+ flexDirection="row"
265
+ onMouseDown={handleTerminalHeaderMouseDown}
266
+ onMouseDrag={onHeaderMouseDrag}
267
+ onMouseDragEnd={onHeaderMouseDragEnd}
268
+ >
233
269
  <Text fg={textColor} selectable={false}>
234
270
  {`${PANE_HEADER_GRIP}${clippedTitle}${padding}`}
235
271
  </Text>
@@ -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
  });
@@ -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}