gloomberb 0.6.1 → 0.7.0

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 (139) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +48 -27
  14. package/src/components/chart/static-chart-surface.tsx +28 -4
  15. package/src/components/chart/stock-chart.test.ts +0 -16
  16. package/src/components/chart/stock-chart.tsx +38 -21
  17. package/src/components/chart/time-axis-label.tsx +43 -0
  18. package/src/components/command-bar/command-bar.test.tsx +71 -8
  19. package/src/components/command-bar/command-bar.tsx +388 -713
  20. package/src/components/command-bar/confirm-body.tsx +51 -0
  21. package/src/components/command-bar/list-model.ts +77 -0
  22. package/src/components/command-bar/view-model.ts +0 -19
  23. package/src/components/command-bar/workflow-ops.ts +4 -0
  24. package/src/components/data-table-stack-view.test.tsx +39 -4
  25. package/src/components/data-table-stack-view.tsx +11 -104
  26. package/src/components/data-table-view.tsx +43 -79
  27. package/src/components/feed-data-table-stack-view.tsx +10 -17
  28. package/src/components/feed-stack-controller.ts +39 -0
  29. package/src/components/index.ts +5 -1
  30. package/src/components/layout/floating-pane.tsx +30 -2
  31. package/src/components/layout/header.tsx +10 -7
  32. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  33. package/src/components/layout/pane-cycle-order.ts +32 -0
  34. package/src/components/layout/pane-footer.tsx +11 -4
  35. package/src/components/layout/pane-header.tsx +5 -5
  36. package/src/components/layout/shell.test.tsx +66 -22
  37. package/src/components/layout/shell.tsx +13 -6
  38. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  39. package/src/components/pane-settings-dialog.tsx +12 -4
  40. package/src/components/pane-template-wizard.test.tsx +3 -4
  41. package/src/components/speedometer-gauge.test.tsx +45 -0
  42. package/src/components/speedometer-gauge.tsx +558 -0
  43. package/src/components/table-view-shared.tsx +158 -0
  44. package/src/components/ticker-list-table-view.tsx +42 -72
  45. package/src/components/ui/confirm-dialog.tsx +66 -0
  46. package/src/components/ui/data-table.tsx +18 -10
  47. package/src/components/ui/index.ts +0 -1
  48. package/src/components/ui/page-stack-view.tsx +39 -10
  49. package/src/components/ui/table-layout.test.ts +16 -0
  50. package/src/components/ui/table-layout.ts +10 -3
  51. package/src/components/ui/ui.test.tsx +100 -20
  52. package/src/core/state/app-state.ts +6 -3
  53. package/src/market-data/coordinator.test.ts +1 -1
  54. package/src/market-data/coordinator.ts +0 -1
  55. package/src/news/aggregator.ts +0 -3
  56. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  57. package/src/plugins/builtin/ai/providers.ts +0 -17
  58. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  59. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  60. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  61. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  62. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  63. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  64. package/src/plugins/builtin/chat-controller.ts +75 -6
  65. package/src/plugins/builtin/chat.test.tsx +6 -6
  66. package/src/plugins/builtin/chat.tsx +29 -20
  67. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  68. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  69. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  70. package/src/plugins/builtin/correlation/index.tsx +1 -6
  71. package/src/plugins/builtin/debug.tsx +1 -3
  72. package/src/plugins/builtin/econ/index.tsx +1 -2
  73. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  74. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  75. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  76. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  77. package/src/plugins/builtin/help.test.tsx +2 -2
  78. package/src/plugins/builtin/help.tsx +2 -2
  79. package/src/plugins/builtin/holders/index.tsx +10 -15
  80. package/src/plugins/builtin/insider/index.tsx +22 -89
  81. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  82. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  83. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  84. package/src/plugins/builtin/news.tsx +4 -16
  85. package/src/plugins/builtin/notes.tsx +18 -55
  86. package/src/plugins/builtin/options.test.tsx +58 -2
  87. package/src/plugins/builtin/options.tsx +108 -49
  88. package/src/plugins/builtin/plugin-groups.ts +2 -0
  89. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  90. package/src/plugins/builtin/research/index.tsx +5 -21
  91. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  92. package/src/plugins/builtin/sec.tsx +13 -66
  93. package/src/plugins/builtin/sectors/index.tsx +1 -1
  94. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  95. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  96. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  97. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  98. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  99. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  100. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  101. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  102. package/src/plugins/ibkr/gateway-service.ts +1 -1
  103. package/src/plugins/ibkr/index.test.tsx +0 -2
  104. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  105. package/src/plugins/pane-manager.ts +0 -13
  106. package/src/plugins/plugin-runtime.test.tsx +6 -6
  107. package/src/plugins/plugin-runtime.tsx +13 -8
  108. package/src/plugins/prediction-markets/cli.ts +0 -1
  109. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  110. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  111. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  112. package/src/plugins/registry.ts +10 -39
  113. package/src/react/input.ts +1 -0
  114. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  115. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  116. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  117. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  118. package/src/renderers/electrobun/view/key-event.ts +1 -1
  119. package/src/renderers/electrobun/view/styles.css +331 -0
  120. package/src/renderers/opentui/chart-surface.tsx +179 -0
  121. package/src/renderers/opentui/start.tsx +48 -30
  122. package/src/renderers/opentui/test-utils.tsx +17 -0
  123. package/src/renderers/opentui/ui-host.tsx +5 -2
  124. package/src/sources/provider-router.ts +0 -4
  125. package/src/state/app-context.test.ts +45 -0
  126. package/src/state/use-inline-tickers.test.tsx +73 -0
  127. package/src/state/use-inline-tickers.ts +7 -1
  128. package/src/test-support/plugin-runtime.ts +3 -2
  129. package/src/theme/colors.ts +12 -3
  130. package/src/theme/themes.test.ts +23 -1
  131. package/src/theme/themes.ts +115 -0
  132. package/src/types/broker.ts +1 -1
  133. package/src/types/data-provider.ts +0 -1
  134. package/src/types/plugin.ts +0 -2
  135. package/src/ui/host.tsx +1 -0
  136. package/src/utils/back-navigation.ts +24 -1
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  139. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,6 +1,8 @@
1
1
 
2
2
  export { PriceSelectorDialog } from "./price-selector-dialog";
3
3
  export { StaticChartSurface } from "./chart/static-chart-surface";
4
+ export { SpeedometerGauge } from "./speedometer-gauge";
5
+ export type { SpeedometerSegment } from "./speedometer-gauge";
4
6
  export type { QuoteFlashDirection } from "./ticker-list-table";
5
7
  export { TickerListTableView } from "./ticker-list-table-view";
6
8
  export type { TickerListVisibleRange } from "./ticker-list-table-view";
@@ -10,14 +12,16 @@ export type { DataTableKeyEvent } from "./data-table-view";
10
12
  export { DataTableStackView } from "./data-table-stack-view";
11
13
  export { FeedDataTableStackView } from "./feed-data-table-stack-view";
12
14
  export type { FeedDataTableItem } from "./feed-data-table-stack-view";
15
+ export { activeStackIndex, sortStackItems } from "./feed-stack-controller";
16
+ export type { StackSortPreference } from "./feed-stack-controller";
13
17
  export { PaneFooterScope, usePaneFooter } from "./layout/pane-footer";
14
18
  export type { PaneFooterSegment, PaneHint } from "./layout/pane-footer";
15
19
  export { useExternalLinkFooter } from "./use-external-link-footer";
16
20
  export { Button } from "./ui/button";
21
+ export { ConfirmDialog } from "./ui/confirm-dialog";
17
22
  export { ChoiceDialog } from "./ui/choice-dialog";
18
23
  export type { ChoiceDialogChoice, ChoiceDialogProps } from "./ui/choice-dialog";
19
24
  export type { DataTableCell, DataTableColumn } from "./ui/data-table";
20
- export { DialogFrame } from "./ui/frame";
21
25
  export { EmptyState } from "./ui/status";
22
26
  export { getMessageComposerBlockHeight, MessageComposer } from "./ui/message-composer";
23
27
  export { NumberField, TextField } from "./ui/fields";
@@ -30,6 +30,24 @@ interface FloatingPaneWrapperProps {
30
30
  children: ReactNode;
31
31
  }
32
32
 
33
+ function TerminalFloatingPaneBorder({ width, height }: { width: number; height: number }) {
34
+ const borderWidth = Math.max(0, Math.floor(width));
35
+ const borderHeight = Math.max(0, Math.floor(height));
36
+ const bodyHeight = Math.max(0, borderHeight - 2);
37
+ if (borderWidth < 2 || bodyHeight <= 0) return null;
38
+
39
+ return (
40
+ <>
41
+ <Box position="absolute" top={1} left={0} width={1} height={bodyHeight}>
42
+ <Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
43
+ </Box>
44
+ <Box position="absolute" top={1} left={borderWidth - 1} width={1} height={bodyHeight}>
45
+ <Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
46
+ </Box>
47
+ </>
48
+ );
49
+ }
50
+
33
51
  /** Pure visual wrapper; Shell owns the interaction state and supplies handlers. */
34
52
  export function FloatingPaneWrapper({
35
53
  paneId,
@@ -101,7 +119,17 @@ export function FloatingPaneWrapper({
101
119
  {children}
102
120
  </Box>
103
121
 
104
- {reserveFooter && <PaneFooterBar footer={footer} focused={focused} width={width} reserveRight={2} />}
122
+ {reserveFooter && (
123
+ <PaneFooterBar
124
+ footer={footer}
125
+ focused={focused}
126
+ width={width}
127
+ reserveRight={2}
128
+ showBorder={!nativePaneChrome && !focused}
129
+ />
130
+ )}
131
+
132
+ {!nativePaneChrome && !focused && <TerminalFloatingPaneBorder width={width} height={height} />}
105
133
 
106
134
  {nativePaneChrome ? (
107
135
  <Box
@@ -117,7 +145,7 @@ export function FloatingPaneWrapper({
117
145
  />
118
146
  ) : (
119
147
  <Box position="absolute" bottom={0} right={0} width={2} height={1}>
120
- <Text fg={focused ? colors.borderFocused : colors.textDim} selectable={false}>{focused ? "─◢" : " ◢"}</Text>
148
+ <Text fg={focused ? colors.borderFocused : colors.border} selectable={false}>{"─◢"}</Text>
121
149
  </Box>
122
150
  )}
123
151
  </Box>
@@ -1,6 +1,6 @@
1
1
  import { Box, SpinnerMark, Text, TextAttributes, useUiCapabilities } from "../../ui";
2
2
  import { useEffect, type ReactNode } from "react";
3
- import { colors, priceColor } from "../../theme/colors";
3
+ import { blendHex, colors, priceColor } from "../../theme/colors";
4
4
  import { useAppActive } from "../../state/app-activity";
5
5
  import { useAppDispatch, useAppSelector } from "../../state/app-context";
6
6
  import {
@@ -23,8 +23,8 @@ const UPDATE_NOTICE_DURATION_MS = 5_000;
23
23
 
24
24
  function DesktopHeaderPill({
25
25
  children,
26
- backgroundColor = "rgba(8, 12, 15, 0.34)",
27
- borderColor = "rgba(132, 145, 161, 0.22)",
26
+ backgroundColor = blendHex(colors.header, colors.bg, 0.28),
27
+ borderColor = blendHex(colors.border, colors.headerText, 0.22),
28
28
  }: {
29
29
  children: ReactNode;
30
30
  backgroundColor?: string;
@@ -173,8 +173,8 @@ export function Header() {
173
173
  Gloomberb
174
174
  </Text>
175
175
  <DesktopHeaderPill
176
- backgroundColor="rgba(84, 201, 159, 0.12)"
177
- borderColor="rgba(84, 201, 159, 0.28)"
176
+ backgroundColor={blendHex(colors.header, colors.headerText, 0.1)}
177
+ borderColor={blendHex(colors.border, colors.headerText, 0.28)}
178
178
  >
179
179
  <Text fg={colors.headerText} style={{ fontSize: 11 }}>v{VERSION}</Text>
180
180
  </DesktopHeaderPill>
@@ -184,8 +184,11 @@ export function Header() {
184
184
  </Box>
185
185
  {mktLabel ? (
186
186
  <Box paddingRight={1}>
187
- <DesktopHeaderPill borderColor="rgba(132, 145, 161, 0.18)">
188
- <Text fg={mktColor}>{mktLabel}</Text>
187
+ <DesktopHeaderPill
188
+ backgroundColor={blendHex(colors.header, mktColor, 0.12)}
189
+ borderColor={blendHex(colors.border, mktColor, 0.3)}
190
+ >
191
+ <Text fg={mktColor} style={{ fontSize: 11, fontWeight: 700 }}>{mktLabel}</Text>
189
192
  </DesktopHeaderPill>
190
193
  </Box>
191
194
  ) : null}
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { PluginRegistry } from "../../plugins/registry";
3
+ import { createDefaultConfig } from "../../types/config";
4
+ import { getVisiblePaneCycleOrder } from "./pane-cycle-order";
5
+
6
+ function createRegistry(options: {
7
+ paneIds: readonly string[];
8
+ disabledPaneIds?: Record<string, readonly string[]>;
9
+ }): PluginRegistry {
10
+ return {
11
+ panes: new Map(options.paneIds.map((paneId) => [paneId, {}])),
12
+ getPluginPaneIds: (pluginId: string) => [...(options.disabledPaneIds?.[pluginId] ?? [])],
13
+ } as unknown as PluginRegistry;
14
+ }
15
+
16
+ describe("getVisiblePaneCycleOrder", () => {
17
+ test("skips disabled and unregistered panes when cycling focus", () => {
18
+ const config = createDefaultConfig("/tmp/gloomberb-pane-cycle-order");
19
+ const registry = createRegistry({
20
+ paneIds: ["portfolio-list", "chat", "ticker-detail"],
21
+ disabledPaneIds: { chat: ["chat"] },
22
+ });
23
+
24
+ expect(getVisiblePaneCycleOrder(config.layout, registry, ["chat"])).toEqual([
25
+ "portfolio-list:main",
26
+ "ticker-detail:main",
27
+ "ticker-detail:nvda",
28
+ ]);
29
+ });
30
+ });
@@ -0,0 +1,32 @@
1
+ import { getDockedPaneIds } from "../../plugins/pane-manager";
2
+ import type { PluginRegistry } from "../../plugins/registry";
3
+ import { findPaneInstance, type LayoutConfig } from "../../types/config";
4
+
5
+ function disabledPaneTypes(pluginRegistry: PluginRegistry, disabledPlugins: readonly string[]): Set<string> {
6
+ const paneTypes = new Set<string>();
7
+ for (const pluginId of disabledPlugins) {
8
+ for (const paneId of pluginRegistry.getPluginPaneIds(pluginId)) {
9
+ paneTypes.add(paneId);
10
+ }
11
+ }
12
+ return paneTypes;
13
+ }
14
+
15
+ export function getVisiblePaneCycleOrder(
16
+ layout: LayoutConfig,
17
+ pluginRegistry: PluginRegistry,
18
+ disabledPlugins: readonly string[],
19
+ ): string[] {
20
+ const disabledTypes = disabledPaneTypes(pluginRegistry, disabledPlugins);
21
+ const isVisiblePane = (instanceId: string) => {
22
+ const instance = findPaneInstance(layout, instanceId);
23
+ return !!instance
24
+ && !disabledTypes.has(instance.paneId)
25
+ && pluginRegistry.panes.has(instance.paneId);
26
+ };
27
+
28
+ return [
29
+ ...getDockedPaneIds(layout),
30
+ ...layout.floating.map((entry) => entry.instanceId),
31
+ ].filter(isVisiblePane);
32
+ }
@@ -357,17 +357,24 @@ export function PaneFooterBar({
357
357
  focused,
358
358
  width = 0,
359
359
  reserveRight = 0,
360
+ showBorder = false,
360
361
  }: {
361
362
  footer?: CombinedPaneFooter | null;
362
363
  focused: boolean;
363
364
  width?: number;
364
365
  reserveRight?: number;
366
+ showBorder?: boolean;
365
367
  }) {
366
368
  const { nativePaneChrome } = useUiCapabilities();
367
369
  const resolvedFooter = footer ?? EMPTY_FOOTER;
368
370
  const empty = !hasPaneFooterContent(resolvedFooter);
369
371
  const borderColor = focused ? colors.borderFocused : colors.border;
370
372
  const topBorderColor = colors.border;
373
+ const nativeBackgroundColor = empty
374
+ ? "transparent"
375
+ : focused
376
+ ? blendHex(colors.bg, colors.borderFocused, 0.06)
377
+ : blendHex(colors.panel, colors.border, 0.12);
371
378
  const reservedRight = Math.max(0, reserveRight);
372
379
  const rightPadding = reservedRight > 0 ? reservedRight : 1;
373
380
 
@@ -385,7 +392,7 @@ export function PaneFooterBar({
385
392
  style={{
386
393
  "--pane-footer-border-color": empty ? "transparent" : topBorderColor,
387
394
  borderTop: `1px solid ${empty ? "transparent" : topBorderColor}`,
388
- backgroundColor: empty ? "transparent" : focused ? "rgba(84, 201, 159, 0.05)" : "rgba(20, 25, 30, 0.55)",
395
+ backgroundColor: nativeBackgroundColor,
389
396
  boxShadow: empty ? "none" : "inset 0 1px 0 rgba(255,255,255,0.03)",
390
397
  }}
391
398
  >
@@ -399,15 +406,15 @@ export function PaneFooterBar({
399
406
  );
400
407
  }
401
408
 
402
- if (focused) {
409
+ if (focused || showBorder) {
403
410
  const contentWidth = Math.max(0, Math.floor(width) - 1 - reservedRight - (reservedRight > 0 ? 0 : 1));
404
411
  return (
405
- <Box height={1} width={width} flexDirection="row" data-gloom-role="pane-footer" data-focused="true" data-empty={empty ? "true" : "false"}>
412
+ <Box height={1} width={width} flexDirection="row" data-gloom-role="pane-footer" data-focused={focused ? "true" : "false"} data-empty={empty ? "true" : "false"}>
406
413
  <Text fg={borderColor} selectable={false}>└</Text>
407
414
  <Box width={contentWidth} height={1} overflow="hidden">
408
415
  {empty
409
416
  ? <Text fg={borderColor} selectable={false}>{"─".repeat(contentWidth)}</Text>
410
- : <FooterContent footer={resolvedFooter} focused width={contentWidth} />}
417
+ : <FooterContent footer={resolvedFooter} focused={focused} width={contentWidth} />}
411
418
  </Box>
412
419
  {reservedRight === 0 && <Text fg={borderColor} selectable={false}>┘</Text>}
413
420
  </Box>
@@ -108,7 +108,6 @@ export function PaneHeader({
108
108
  const backgroundColor = floating ? floatingPaneTitleBg(focused) : paneTitleBg(focused);
109
109
  const actionText = showActions ? PANE_HEADER_ACTION : " ";
110
110
  const closeText = floating ? PANE_HEADER_CLOSE : "";
111
- const bc = colors.borderFocused;
112
111
  const textColor = paneTitleText(focused, floating);
113
112
 
114
113
  if (nativePaneChrome) {
@@ -184,9 +183,10 @@ export function PaneHeader({
184
183
  );
185
184
  }
186
185
 
187
- if (focused) {
186
+ if (focused || floating) {
188
187
  // Build: ┌─:: Title ─────────── ... x─┐
189
188
  // Reserve 2 for corners, 1 for ─ after ┌, 1 for ─ before ┐
189
+ const borderColor = focused ? colors.borderFocused : colors.border;
190
190
  const innerWidth = Math.max(0, width - 4);
191
191
  const contentWidth = PANE_HEADER_GRIP.length + closeText.length + actionText.length;
192
192
  const titleWidth = Math.max(0, innerWidth - contentWidth);
@@ -196,9 +196,9 @@ export function PaneHeader({
196
196
 
197
197
  return (
198
198
  <Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
199
- <Text fg={bc} selectable={false}>{"┌─"}</Text>
199
+ <Text fg={borderColor} selectable={false}>{"┌─"}</Text>
200
200
  <Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
201
- <Text fg={bc} selectable={false}>{fill}</Text>
201
+ <Text fg={borderColor} selectable={false}>{fill}</Text>
202
202
  <TerminalPaneButton
203
203
  text={actionText}
204
204
  fg={textColor}
@@ -213,7 +213,7 @@ export function PaneHeader({
213
213
  onMouseDown={onCloseMouseDown}
214
214
  />
215
215
  )}
216
- <Text fg={bc} selectable={false}>{"─┐"}</Text>
216
+ <Text fg={borderColor} selectable={false}>{"─┐"}</Text>
217
217
  </Box>
218
218
  );
219
219
  }
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { TextAttributes } from "@opentui/core";
3
- import { testRender } from "../../renderers/opentui/test-utils";
4
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
+ import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
5
4
  import type { ReactNode } from "react";
6
5
  import { act, createElement, useReducer } from "react";
7
6
  import { AppContext, appReducer, createInitialState } from "../../state/app-context";
@@ -76,7 +75,7 @@ function createShellPluginRegistry(options?: {
76
75
  openPaneSettingsFn: () => {},
77
76
  openCommandBar: () => {},
78
77
  updateLayoutFn: () => {},
79
- hideWidget: () => {},
78
+ hidePane: () => {},
80
79
  } as unknown as PluginRegistry;
81
80
  }
82
81
 
@@ -114,7 +113,7 @@ function createBrokerPortfolioRegistry(): PluginRegistry {
114
113
  openPaneSettingsFn: () => {},
115
114
  openCommandBar: () => {},
116
115
  updateLayoutFn: () => {},
117
- hideWidget: () => {},
116
+ hidePane: () => {},
118
117
  focusPaneFn: () => {},
119
118
  pinTicker: () => {},
120
119
  navigateTicker: () => {},
@@ -337,9 +336,9 @@ function ChartShellHarness({
337
336
 
338
337
  return (
339
338
  <AppContext value={{ state, dispatch }}>
340
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
339
+ <TestDialogProvider>
341
340
  <Shell pluginRegistry={pluginRegistry} />
342
- </DialogProvider>
341
+ </TestDialogProvider>
343
342
  </AppContext>
344
343
  );
345
344
  }
@@ -613,9 +612,9 @@ describe("Shell", () => {
613
612
 
614
613
  testSetup = await testRender(
615
614
  <AppContext value={{ state, dispatch: () => {} }}>
616
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
615
+ <TestDialogProvider>
617
616
  <Shell pluginRegistry={pluginRegistry} />
618
- </DialogProvider>
617
+ </TestDialogProvider>
619
618
  </AppContext>,
620
619
  { width: 40, height: 10 },
621
620
  );
@@ -655,7 +654,7 @@ describe("Shell", () => {
655
654
 
656
655
  testSetup = await testRender(
657
656
  <AppContext value={{ state, dispatch: () => {} }}>
658
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
657
+ <TestDialogProvider>
659
658
  <Shell
660
659
  pluginRegistry={pluginRegistry}
661
660
  desktopWindowBridge={{
@@ -665,7 +664,7 @@ describe("Shell", () => {
665
664
  subscribeDockPreview: () => () => {},
666
665
  }}
667
666
  />
668
- </DialogProvider>
667
+ </TestDialogProvider>
669
668
  </AppContext>,
670
669
  { width: 40, height: 10 },
671
670
  );
@@ -704,9 +703,9 @@ describe("Shell", () => {
704
703
 
705
704
  testSetup = await testRender(
706
705
  <AppContext value={{ state, dispatch: () => {} }}>
707
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
706
+ <TestDialogProvider>
708
707
  <Shell pluginRegistry={pluginRegistry} />
709
- </DialogProvider>
708
+ </TestDialogProvider>
710
709
  </AppContext>,
711
710
  { width: 40, height: 10 },
712
711
  );
@@ -823,9 +822,9 @@ describe("Shell", () => {
823
822
  setSharedRegistryForTests(pluginRegistry);
824
823
 
825
824
  testSetup = await testRender(
826
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
825
+ <TestDialogProvider>
827
826
  <BrokerShellHarness pluginRegistry={pluginRegistry} />
828
- </DialogProvider>,
827
+ </TestDialogProvider>,
829
828
  { width: 100, height: 24 },
830
829
  );
831
830
 
@@ -916,9 +915,9 @@ describe("Shell", () => {
916
915
 
917
916
  testSetup = await testRender(
918
917
  <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
919
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
918
+ <TestDialogProvider>
920
919
  <Shell pluginRegistry={createShellPluginRegistry()} />
921
- </DialogProvider>
920
+ </TestDialogProvider>
922
921
  </AppContext>,
923
922
  { width: 40, height: 10 },
924
923
  );
@@ -946,6 +945,7 @@ describe("Shell", () => {
946
945
  dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
947
946
  instances: [{ ...mainPane }, { ...detailPane }],
948
947
  floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
948
+ detached: [],
949
949
  };
950
950
  const state = {
951
951
  ...createInitialState({
@@ -959,9 +959,9 @@ describe("Shell", () => {
959
959
 
960
960
  testSetup = await testRender(
961
961
  <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
962
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
962
+ <TestDialogProvider>
963
963
  <Shell pluginRegistry={createShellPluginRegistry()} />
964
- </DialogProvider>
964
+ </TestDialogProvider>
965
965
  </AppContext>,
966
966
  { width: 40, height: 12 },
967
967
  );
@@ -989,6 +989,7 @@ describe("Shell", () => {
989
989
  dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
990
990
  instances: [{ ...mainPane }, { ...detailPane }],
991
991
  floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
992
+ detached: [],
992
993
  };
993
994
  const state = {
994
995
  ...createInitialState({
@@ -1002,9 +1003,9 @@ describe("Shell", () => {
1002
1003
 
1003
1004
  testSetup = await testRender(
1004
1005
  <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
1005
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
1006
+ <TestDialogProvider>
1006
1007
  <Shell pluginRegistry={createShellPluginRegistry()} />
1007
- </DialogProvider>
1008
+ </TestDialogProvider>
1008
1009
  </AppContext>,
1009
1010
  { width: 40, height: 12 },
1010
1011
  );
@@ -1020,6 +1021,49 @@ describe("Shell", () => {
1020
1021
  expect(updateLayout?.layout.floating).toEqual([]);
1021
1022
  });
1022
1023
 
1024
+ test("closes the focused floating pane with Ctrl+W while plugin input is captured", async () => {
1025
+ const config = createDefaultConfig("/tmp/gloomberb-shell-test");
1026
+ const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
1027
+ const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
1028
+ if (!mainPane || !detailPane) throw new Error("missing default panes");
1029
+
1030
+ const mixedLayout = {
1031
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
1032
+ instances: [{ ...mainPane }, { ...detailPane }],
1033
+ floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
1034
+ detached: [],
1035
+ };
1036
+ const state = {
1037
+ ...createInitialState({
1038
+ ...config,
1039
+ layout: cloneLayout(mixedLayout),
1040
+ layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
1041
+ }),
1042
+ focusedPaneId: "ticker-detail:main",
1043
+ inputCaptured: true,
1044
+ };
1045
+ const actions: Array<any> = [];
1046
+
1047
+ testSetup = await testRender(
1048
+ <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
1049
+ <TestDialogProvider>
1050
+ <Shell pluginRegistry={createShellPluginRegistry()} />
1051
+ </TestDialogProvider>
1052
+ </AppContext>,
1053
+ { width: 40, height: 12 },
1054
+ );
1055
+
1056
+ await testSetup.renderOnce();
1057
+ await act(async () => {
1058
+ testSetup!.mockInput.pressKey("w", { ctrl: true });
1059
+ await testSetup!.renderOnce();
1060
+ });
1061
+
1062
+ const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
1063
+ expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
1064
+ expect(updateLayout?.layout.floating).toEqual([]);
1065
+ });
1066
+
1023
1067
  test("keeps a floating pane at the preview rect after a free drag release", () => {
1024
1068
  const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
1025
1069
  const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
@@ -1153,7 +1197,7 @@ describe("Shell", () => {
1153
1197
 
1154
1198
  testSetup = await testRender(
1155
1199
  <AppContext value={{ state, dispatch: () => {} }}>
1156
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
1200
+ <TestDialogProvider>
1157
1201
  <Shell pluginRegistry={createShellPluginRegistry({
1158
1202
  tickerDetailComponent: ({ width, height }) => (
1159
1203
  <box flexDirection="column" width={width} height={height}>
@@ -1164,7 +1208,7 @@ describe("Shell", () => {
1164
1208
  </box>
1165
1209
  ),
1166
1210
  })} />
1167
- </DialogProvider>
1211
+ </TestDialogProvider>
1168
1212
  </AppContext>,
1169
1213
  { width: 40, height: 12 },
1170
1214
  );
@@ -9,7 +9,6 @@ import {
9
9
  MIN_FLOAT_HEIGHT,
10
10
  MIN_FLOAT_WIDTH,
11
11
  applyDrop,
12
- findDockLeaf,
13
12
  floatAtRect,
14
13
  floatPane,
15
14
  getDockDividerLayouts,
@@ -22,7 +21,6 @@ import {
22
21
  resolveDocked,
23
22
  resolveFloating,
24
23
  simulateDrop,
25
- swapPanes,
26
24
  type DockDividerLayout,
27
25
  type DockLeafLayout,
28
26
  type DropTarget,
@@ -218,6 +216,14 @@ export function resolvePaneManagementShortcut(
218
216
  return null;
219
217
  }
220
218
 
219
+ function inputCaptureAllowsPaneManagementShortcut(
220
+ shortcut: PaneManagementShortcut,
221
+ event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
222
+ ): boolean {
223
+ if (shortcut !== "close") return false;
224
+ return event.meta || event.super || event.targetEditable !== true;
225
+ }
226
+
221
227
  export function resolveAppHeaderHeightCells(options: { titleBarOverlay?: boolean; cellHeightPx?: number }): number {
222
228
  if (!options.titleBarOverlay || !options.cellHeightPx || options.cellHeightPx <= 0) return DEFAULT_HEADER_HEIGHT;
223
229
  return TITLEBAR_OVERLAY_HEIGHT_PX / options.cellHeightPx;
@@ -588,8 +594,8 @@ function resolveHeaderHitAreas(
588
594
  actionStart: number | null;
589
595
  closeStart: number | null;
590
596
  } {
591
- // When focused, the header renders ┌─...─┐ adding 2 chars on each side
592
- let rightEdge = options.focused ? width - 2 : width;
597
+ // Focused panes and all floating panes render ┌─...─┐, adding 2 chars on each side.
598
+ let rightEdge = options.focused || options.floating ? width - 2 : width;
593
599
  let closeStart: number | null = null;
594
600
  let actionStart: number | null = null;
595
601
 
@@ -966,7 +972,8 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
966
972
  }
967
973
 
968
974
  const shortcut = resolvePaneManagementShortcut(event);
969
- if (!shortcut || dragRef.current || overlayOpen || inputCaptured) return;
975
+ if (!shortcut || dragRef.current || overlayOpen) return;
976
+ if (inputCaptured && !inputCaptureAllowsPaneManagementShortcut(shortcut, event)) return;
970
977
 
971
978
  let handled = false;
972
979
  switch (shortcut) {
@@ -1701,7 +1708,7 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
1701
1708
  "data-active": active ? "true" : "false",
1702
1709
  style: { "--divider-color": active ? colors.borderFocused : colors.border } as any,
1703
1710
  } : {})}
1704
- onMouseDown={nativePaneChrome ? (event) => startNativeDividerDrag(divider, event) : undefined}
1711
+ onMouseDown={nativePaneChrome ? (event: ShellMouseEvent) => startNativeDividerDrag(divider, event) : undefined}
1705
1712
  onMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
1706
1713
  onMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
1707
1714
  />
@@ -1,4 +1,4 @@
1
- import { AsciiText, Box, Input, Span, Strong, Text, Underline, useUiHost } from "../../ui";
1
+ import { AsciiText, Box, Span, Strong, Text, Underline, useUiHost } from "../../ui";
2
2
  import { useState, useCallback, useEffect, useRef, useMemo, type RefObject } from "react";
3
3
  import { useShortcut, useViewport } from "../../react/input";
4
4
  import { TextAttributes } from "../../ui";
@@ -963,7 +963,6 @@ function PortfolioStep({
963
963
 
964
964
  // broker-fields: dynamically render fields from the broker's configSchema
965
965
  if (!selectedBrokerId) return null;
966
- const currentField = brokerFields[brokerFieldIdx];
967
966
  const values = brokerValues[selectedBrokerId] ?? {};
968
967
 
969
968
  return (
@@ -9,7 +9,7 @@ import type {
9
9
  PaneSettingTextField,
10
10
  } from "../types/plugin";
11
11
  import type { PluginRegistry } from "../plugins/registry";
12
- import { colors } from "../theme/colors";
12
+ import { blendHex, colors } from "../theme/colors";
13
13
  import { Button, DialogFrame, ListView, MultiSelectDialogContent, TextField } from "./ui";
14
14
  import { NativeSelect, openNativeSelect, type NativeSelectElement } from "./ui/native-select";
15
15
 
@@ -80,6 +80,14 @@ function desktopText(weight?: number) {
80
80
  return weight ? { ...DESKTOP_TEXT_STYLE, fontWeight: weight } : DESKTOP_TEXT_STYLE;
81
81
  }
82
82
 
83
+ function desktopSubtleSurface(): string {
84
+ return blendHex(colors.panel, colors.bg, 0.22);
85
+ }
86
+
87
+ function desktopHoverSurface(): string {
88
+ return blendHex(colors.bg, colors.borderFocused, 0.08);
89
+ }
90
+
83
91
  function DesktopDialogSurface({
84
92
  title,
85
93
  subtitle,
@@ -149,7 +157,7 @@ function DesktopSwitch({
149
157
  flexDirection="row"
150
158
  alignItems="center"
151
159
  justifyContent={checked ? "flex-end" : "flex-start"}
152
- backgroundColor={checked ? colors.selected : "rgba(255, 255, 255, 0.07)"}
160
+ backgroundColor={checked ? colors.selected : desktopSubtleSurface()}
153
161
  onMouseDown={(event: any) => {
154
162
  event.stopPropagation?.();
155
163
  event.preventDefault?.();
@@ -182,7 +190,7 @@ function DesktopValuePill({ value }: { value: string }) {
182
190
  <Box
183
191
  flexDirection="row"
184
192
  alignItems="center"
185
- backgroundColor="rgba(255, 255, 255, 0.06)"
193
+ backgroundColor={desktopSubtleSurface()}
186
194
  style={{
187
195
  border: `1px solid ${colors.border}`,
188
196
  borderRadius: 6,
@@ -236,7 +244,7 @@ function DesktopSettingsRow({
236
244
  <Box
237
245
  flexDirection="column"
238
246
  minHeight={field.description ? undefined : 2}
239
- backgroundColor={hovered || selected ? "rgba(255, 255, 255, 0.045)" : "transparent"}
247
+ backgroundColor={hovered || selected ? desktopHoverSurface() : "transparent"}
240
248
  onMouseMove={onHover}
241
249
  onMouseDown={field.type === "select" ? undefined : (event: any) => {
242
250
  event.stopPropagation?.();
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act } from "react";
3
- import { testRender } from "../renderers/opentui/test-utils";
4
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
+ import { TestDialogProvider, testRender } from "../renderers/opentui/test-utils";
5
4
  import { PaneTemplateTextareaStep } from "./pane-template-wizard";
6
5
 
7
6
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
@@ -18,7 +17,7 @@ describe("PaneTemplateTextareaStep", () => {
18
17
  let resolved = "";
19
18
 
20
19
  testSetup = await testRender(
21
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
20
+ <TestDialogProvider>
22
21
  <PaneTemplateTextareaStep
23
22
  resolve={(value) => { resolved = value; }}
24
23
  dialogId="textarea-test"
@@ -30,7 +29,7 @@ describe("PaneTemplateTextareaStep", () => {
30
29
  defaultValue: "Find cash-generative semiconductor suppliers",
31
30
  }}
32
31
  />
33
- </DialogProvider>,
32
+ </TestDialogProvider>,
34
33
  { width: 80, height: 16 },
35
34
  );
36
35