gloomberb 0.6.0 → 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 (156) hide show
  1. package/package.json +2 -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 +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -27,7 +27,7 @@ import {
27
27
  getKeyboardPanCellCount,
28
28
  resolveDragPanOffset,
29
29
  } from "./chart-scroll";
30
- import { getVisibleWindow, projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
30
+ import { projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
31
31
  import {
32
32
  buildPresetDateWindow,
33
33
  buildVisibleDateWindow,
@@ -110,7 +110,6 @@ import {
110
110
  } from "./chart-types";
111
111
  import {
112
112
  computeBitmapSize,
113
- intersectCellRects,
114
113
  renderNativeChartBase,
115
114
  renderNativeCrosshairOverlay,
116
115
  type CellRect,
@@ -121,8 +120,14 @@ import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-suppor
121
120
  import { resolveChartRendererState } from "./native/renderer-selection";
122
121
  import { getNativeSurfaceManager } from "./native/surface-manager";
123
122
  import { syncCachedNativeSurface } from "./native/surface-sync";
123
+ import {
124
+ getRenderableCellRect,
125
+ resolveNativeSurfaceVisibleRect,
126
+ type NativeSurfaceRenderableNode,
127
+ } from "./native/surface-visibility";
124
128
  import type { PricePoint, TickerFinancials } from "../../types/financials";
125
129
  import type { TickerRecord } from "../../types/ticker";
130
+ import { TimeAxisLabel } from "./time-axis-label";
126
131
 
127
132
  const MODE_CHIPS: Record<ChartRenderMode, string> = {
128
133
  area: "A",
@@ -169,7 +174,6 @@ interface StockChartProps {
169
174
  }
170
175
 
171
176
  interface ResolvedStockChartProps extends StockChartProps {
172
- symbol: string | null;
173
177
  ticker: TickerRecord | null;
174
178
  financials: TickerFinancials | null;
175
179
  }
@@ -275,14 +279,6 @@ interface DisplayCursorState {
275
279
  pixelY: number | null;
276
280
  }
277
281
 
278
- interface RenderableNode {
279
- x: number;
280
- y: number;
281
- width: number;
282
- height: number;
283
- parent: RenderableNode | null;
284
- }
285
-
286
282
  const EMPTY_DISPLAY_CURSOR: DisplayCursorState = {
287
283
  cellX: null,
288
284
  cellY: null,
@@ -522,41 +518,6 @@ function resolveCursorMotionKind(
522
518
  : "cell";
523
519
  }
524
520
 
525
- function extractCellRect(renderable: RenderableNode): CellRect {
526
- return {
527
- x: renderable.x,
528
- y: renderable.y,
529
- width: renderable.width,
530
- height: renderable.height,
531
- };
532
- }
533
-
534
- function resolveVisibleRect(
535
- renderable: RenderableNode | null,
536
- terminalWidth: number,
537
- terminalHeight: number,
538
- ): CellRect | null {
539
- if (!renderable) return null;
540
-
541
- let visible: CellRect = {
542
- x: 0,
543
- y: 0,
544
- width: terminalWidth,
545
- height: terminalHeight,
546
- };
547
- let current: RenderableNode | null = renderable;
548
-
549
- while (current) {
550
- const currentRect = extractCellRect(current);
551
- const nextVisible = intersectCellRects(visible, currentRect);
552
- if (!nextVisible) return null;
553
- visible = nextVisible;
554
- current = current.parent;
555
- }
556
-
557
- return visible;
558
- }
559
-
560
521
  function buildBlankPlotLines(width: number, height: number): string[] {
561
522
  return Array.from({ length: height }, () => " ".repeat(width));
562
523
  }
@@ -765,14 +726,6 @@ function isSeriesAcceptedForRequest(
765
726
  return windowPoints.length >= minimumPointCount;
766
727
  }
767
728
 
768
- function canRenderAutoViewForWindow(
769
- render: AutoRenderedView | null,
770
- requestedWindow: DateWindowRange | null | undefined,
771
- ): render is AutoRenderedView {
772
- if (!render || !requestedWindow?.start || !requestedWindow.end) return false;
773
- return getWindowPoints(render.data, requestedWindow).length > 0;
774
- }
775
-
776
729
  export function resolveAutoDisplayState(options: {
777
730
  shouldUseRenderedAutoView: boolean;
778
731
  renderedAutoView: AutoRenderedView | null;
@@ -811,13 +764,6 @@ export function resolveAutoDisplayState(options: {
811
764
  };
812
765
  }
813
766
 
814
- export function resolveVisibleChartDateWindow(
815
- points: readonly Pick<PricePoint, "date">[],
816
- preferredWindow: DateWindowRange | null | undefined,
817
- ): DateWindowRange | null {
818
- return getDateWindowBounds(getPointDates(points)) ?? preferredWindow ?? null;
819
- }
820
-
821
767
  function buildDateWindowFromIndices(dates: readonly Date[], startIdx: number, endIdx: number): DateWindowRange | null {
822
768
  if (dates.length === 0 || endIdx <= startIdx) return null;
823
769
  return {
@@ -1469,8 +1415,8 @@ function buildIndicatorProjectionKey(options: {
1469
1415
  }
1470
1416
 
1471
1417
  export function StockChart(props: StockChartProps) {
1472
- const { symbol, ticker, financials } = usePaneTicker();
1473
- return <ResolvedStockChart {...props} symbol={symbol} ticker={ticker} financials={financials} />;
1418
+ const { ticker, financials } = usePaneTicker();
1419
+ return <ResolvedStockChart {...props} ticker={ticker} financials={financials} />;
1474
1420
  }
1475
1421
 
1476
1422
  export const ResolvedStockChart = memo(function ResolvedStockChart({
@@ -1487,7 +1433,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1487
1433
  showVolume: showVolumeOverride,
1488
1434
  footerControls,
1489
1435
  footerHints,
1490
- symbol,
1491
1436
  ticker,
1492
1437
  financials,
1493
1438
  }: ResolvedStockChartProps) {
@@ -1553,6 +1498,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1553
1498
  const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1554
1499
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1555
1500
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1501
+ const mouseCrosshairDisabledRef = useRef(false);
1556
1502
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
1557
1503
  const animationFrameRef = useRef<number | null>(null);
1558
1504
  const pendingCanonicalResetRef = useRef(1);
@@ -2945,6 +2891,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2945
2891
  if (!interactive) return;
2946
2892
 
2947
2893
  switch (key) {
2894
+ case "escape":
2895
+ mouseCrosshairDisabledRef.current = true;
2896
+ updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2897
+ commitSelectionCursor({ cursorX: null, cursorY: null });
2898
+ return;
2948
2899
  case "left":
2949
2900
  if (event.shift) {
2950
2901
  if (effectiveResolution === "auto") {
@@ -2962,6 +2913,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2962
2913
  return { ...cleared, panOffset: current.panOffset + panStep };
2963
2914
  });
2964
2915
  } else {
2916
+ mouseCrosshairDisabledRef.current = false;
2965
2917
  cursorMotionKindRef.current = "discrete";
2966
2918
  const pointCount = projection.points.length;
2967
2919
  const currentIndex = pointCount <= 0
@@ -3013,6 +2965,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3013
2965
  return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
3014
2966
  });
3015
2967
  } else {
2968
+ mouseCrosshairDisabledRef.current = false;
3016
2969
  cursorMotionKindRef.current = "discrete";
3017
2970
  const pointCount = projection.points.length;
3018
2971
  const currentIndex = pointCount <= 0
@@ -3097,6 +3050,19 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3097
3050
  colors: chartColors,
3098
3051
  timeAxisDates,
3099
3052
  }), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
3053
+ const displayScene = useMemo(() => buildChartScene(projection.points, {
3054
+ width: chartWidth,
3055
+ height: chartHeight,
3056
+ showVolume: showVolume && !compact,
3057
+ volumeHeight,
3058
+ cursorX: displayCursorX,
3059
+ cursorY: displayCursorY,
3060
+ mode: projection.effectiveMode,
3061
+ axisMode,
3062
+ colors: chartColors,
3063
+ timeAxisDates,
3064
+ indicators,
3065
+ }), [axisMode, chartColors, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3100
3066
  const snappedSelectionCursorX = selectionScene
3101
3067
  ? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
3102
3068
  : null;
@@ -3236,8 +3202,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3236
3202
 
3237
3203
  const syncPlacement = () => {
3238
3204
  if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
3239
- const rect = extractCellRect(plotRef.current);
3240
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3205
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3206
+ const rect = getRenderableCellRect(plot);
3207
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3241
3208
  const previous = lastNativeGeometryRef.current;
3242
3209
  if (previous
3243
3210
  && previous.rect.x === rect.x
@@ -3292,8 +3259,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3292
3259
  return;
3293
3260
  }
3294
3261
 
3295
- const plotRect = extractCellRect(plotRef.current);
3296
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3262
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3263
+ const plotRect = getRenderableCellRect(plot);
3264
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3297
3265
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3298
3266
  const bitmapKey = buildNativeBitmapKey(
3299
3267
  projection.points.length,
@@ -3364,8 +3332,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3364
3332
  return;
3365
3333
  }
3366
3334
 
3367
- const plotRect = extractCellRect(plotRef.current);
3368
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
3335
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
3336
+ const plotRect = getRenderableCellRect(plot);
3337
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
3369
3338
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
3370
3339
  const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
3371
3340
  const overlayPixelX = scaleLocalPixelCoordinate(
@@ -3455,6 +3424,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3455
3424
  : null;
3456
3425
  const isUpdating = !compact && (renderBodyState.updating || boundsBodyState.updating);
3457
3426
  const timeAxisLabel = selectionScene?.timeLabels ?? staticResult.timeLabels;
3427
+ const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
3428
+ const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.dateAtCursor ?? null : null;
3458
3429
  const timeAxisBox = (
3459
3430
  <Box
3460
3431
  height={1}
@@ -3462,7 +3433,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3462
3433
  overflow="hidden"
3463
3434
  data-gloom-role="chart-time-axis"
3464
3435
  >
3465
- <Text fg={colors.textDim} style={{ whiteSpace: "pre" }}>{timeAxisLabel}</Text>
3436
+ <TimeAxisLabel
3437
+ timeLabels={timeAxisLabel}
3438
+ width={chartWidth}
3439
+ cursorColumn={cursorTimeAxisColumn}
3440
+ cursorDate={cursorTimeAxisDate}
3441
+ dates={timeAxisDates}
3442
+ cursorColor={chartColors.crosshairColor}
3443
+ />
3466
3444
  </Box>
3467
3445
  );
3468
3446
  const chartFooterHints = useMemo<PaneHint[]>(() => {
@@ -3609,6 +3587,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3609
3587
 
3610
3588
  const handlePlotMove = (event: ChartMouseEvent) => {
3611
3589
  if (!interactive || compact) return;
3590
+ if (mouseCrosshairDisabledRef.current) return;
3612
3591
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3613
3592
  if (!localPointer) return;
3614
3593
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -3630,6 +3609,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3630
3609
  if (compact) return;
3631
3610
  if (!interactive) onActivate?.();
3632
3611
  focusPaneForMouseInteraction(event);
3612
+ mouseCrosshairDisabledRef.current = false;
3633
3613
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3634
3614
  if (!localPointer) return;
3635
3615
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -3654,6 +3634,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3654
3634
 
3655
3635
  const handlePlotDrag = (event: ChartMouseEvent) => {
3656
3636
  if (compact || (!interactive && !dragRef.current)) return;
3637
+ if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
3657
3638
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3658
3639
  if (localPointer) {
3659
3640
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -0,0 +1,43 @@
1
+ import { useMemo } from "react";
2
+ import { Span, Text } from "../../ui";
3
+ import { colors } from "../../theme/colors";
4
+ import { buildCursorTimeAxisSegments } from "./chart-renderer";
5
+
6
+ interface TimeAxisLabelProps {
7
+ timeLabels: string;
8
+ width: number;
9
+ cursorColumn: number | null;
10
+ cursorDate: Date | string | number | null;
11
+ dates: Array<Date | string | number>;
12
+ cursorColor: string;
13
+ }
14
+
15
+ export function TimeAxisLabel({
16
+ timeLabels,
17
+ width,
18
+ cursorColumn,
19
+ cursorDate,
20
+ dates,
21
+ cursorColor,
22
+ }: TimeAxisLabelProps) {
23
+ const segments = useMemo(() => buildCursorTimeAxisSegments({
24
+ timeLabels,
25
+ width,
26
+ cursorColumn,
27
+ cursorDate,
28
+ dates,
29
+ }), [cursorColumn, cursorDate, dates, timeLabels, width]);
30
+
31
+ return (
32
+ <Text style={{ whiteSpace: "pre" }}>
33
+ {segments.map((segment, index) => (
34
+ <Span
35
+ key={index}
36
+ fg={segment.highlighted ? cursorColor : colors.textDim}
37
+ >
38
+ {segment.text}
39
+ </Span>
40
+ ))}
41
+ </Text>
42
+ );
43
+ }
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useReducer } 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 { CommandBar } from "./command-bar";
6
5
  import { AppContext, type AppState, appReducer, createInitialState, getEffectiveThemeId } from "../../state/app-context";
7
6
  import { createTestDataProvider } from "../../test-support/data-provider";
@@ -213,9 +212,9 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
213
212
  getPluginPaneTemplateIds: () => [],
214
213
  hasPaneSettings,
215
214
  events: { emit: () => {} },
216
- hideWidget: () => {},
215
+ hidePane: () => {},
217
216
  pinTicker: () => {},
218
- showWidget: () => {},
217
+ showPane: () => {},
219
218
  updateLayoutFn: () => {},
220
219
  getTermSizeFn: () => ({ width: 80, height: 24 }),
221
220
  notify: () => {},
@@ -304,7 +303,7 @@ function CommandBarHarness({
304
303
 
305
304
  return (
306
305
  <AppContext value={{ state: currentState, dispatch: currentDispatch }}>
307
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
306
+ <TestDialogProvider>
308
307
  {live && <text>{`theme:${getEffectiveThemeId(currentState)}`}</text>}
309
308
  {showQueryState && <text>{`query:${currentState.commandBarQuery}`}</text>}
310
309
  {currentState.commandBarOpen && (
@@ -316,7 +315,7 @@ function CommandBarHarness({
316
315
  onCheckForUpdates={onCheckForUpdates}
317
316
  />
318
317
  )}
319
- </DialogProvider>
318
+ </TestDialogProvider>
320
319
  </AppContext>
321
320
  );
322
321
  }
@@ -740,7 +739,12 @@ describe("CommandBar", () => {
740
739
  expect(frame).toContain("Commands");
741
740
  expect(frame).toContain("TH");
742
741
  expect(frame).toContain("Themes");
743
- expect(frame).toMatchSnapshot();
742
+ expect(frame).toContain("Amber");
743
+ expect(frame).toContain("Paper");
744
+ expect(frame).toContain("GitHub Light");
745
+ expect(frame).toContain("Solarized Light");
746
+ expect(frame).toContain("Gruvbox Light");
747
+ expect(frame).toContain("Nord Light");
744
748
  });
745
749
 
746
750
  test("renders plugin toggle state", async () => {
@@ -2197,6 +2201,50 @@ describe("CommandBar", () => {
2197
2201
  expect(frame).toContain("AAPL,");
2198
2202
  });
2199
2203
 
2204
+ test("CMP with one resolved ticker opens inline completion instead of creating a one-symbol chart", async () => {
2205
+ const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2206
+
2207
+ testSetup = await testRender(<CommandBarHarness
2208
+ query="CMP AMD"
2209
+ extraTickers={[makeTicker("AMD", "Advanced Micro Devices")]}
2210
+ configurePluginRegistry={(pluginRegistry) => {
2211
+ (pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
2212
+ id: "comparison-chart",
2213
+ name: "Comparison Chart",
2214
+ component: () => null,
2215
+ defaultPosition: "right",
2216
+ });
2217
+ (pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
2218
+ id: "comparison-chart-pane",
2219
+ paneId: "comparison-chart",
2220
+ label: "Comparison Chart",
2221
+ description: "Compare multiple symbols in one pane",
2222
+ shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
2223
+ wizard: [{ key: "tickers", label: "Tickers", type: "text" }],
2224
+ canCreate: (_context: unknown, options?: { symbols?: string[] | null }) => !options?.symbols || options.symbols.length >= 2,
2225
+ });
2226
+ pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2227
+ created.push({ templateId, options });
2228
+ };
2229
+ }}
2230
+ />, {
2231
+ width: 100,
2232
+ height: 20,
2233
+ });
2234
+
2235
+ await testSetup.renderOnce();
2236
+ await act(async () => {
2237
+ testSetup!.mockInput.pressEnter();
2238
+ await Bun.sleep(0);
2239
+ await testSetup!.renderOnce();
2240
+ });
2241
+
2242
+ const frame = await waitForFrameToContain("Tickers");
2243
+ expect(created).toEqual([]);
2244
+ expect(frame).toContain("Comparison Chart");
2245
+ expect(frame).toContain("AMD");
2246
+ });
2247
+
2200
2248
  test("AI <prompt> opens the inline workflow and prefills the textarea prompt", async () => {
2201
2249
  const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
2202
2250
 
@@ -2608,7 +2656,15 @@ describe("CommandBar", () => {
2608
2656
  });
2609
2657
 
2610
2658
  test("matches the direct help command", async () => {
2611
- testSetup = await testRender(<CommandBarHarness query="help" />, {
2659
+ const openedPanes: string[] = [];
2660
+ testSetup = await testRender(<CommandBarHarness
2661
+ query="help"
2662
+ configurePluginRegistry={(pluginRegistry) => {
2663
+ pluginRegistry.showPane = (paneId: string) => {
2664
+ openedPanes.push(paneId);
2665
+ };
2666
+ }}
2667
+ />, {
2612
2668
  width: 80,
2613
2669
  height: 18,
2614
2670
  });
@@ -2618,6 +2674,13 @@ describe("CommandBar", () => {
2618
2674
  const frame = testSetup.captureCharFrame();
2619
2675
  expect(frame).toContain("Help");
2620
2676
  expect(frame).toContain("Navigation");
2677
+
2678
+ await act(async () => {
2679
+ testSetup!.mockInput.pressEnter();
2680
+ await testSetup!.renderOnce();
2681
+ });
2682
+
2683
+ expect(openedPanes).toEqual(["help"]);
2621
2684
  });
2622
2685
 
2623
2686
  test("skips pane templates whose canCreate throws", async () => {