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
@@ -54,12 +54,10 @@ import {
54
54
  normalizeChartResolutionSupport,
55
55
  sortChartResolutions,
56
56
  type ChartResolutionSupport,
57
- type ManualChartResolution,
58
57
  } from "./chart-resolution";
59
58
  import { RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
60
59
  import {
61
60
  buildComparisonChartScene,
62
- formatComparisonAxisValue,
63
61
  formatComparisonCursorAxisValue,
64
62
  renderComparisonChart,
65
63
  } from "./comparison-chart-renderer";
@@ -82,7 +80,6 @@ import {
82
80
  } from "./chart-types";
83
81
  import {
84
82
  computeBitmapSize,
85
- intersectCellRects,
86
83
  renderNativeComparisonChartBase,
87
84
  renderNativeCrosshairOverlay,
88
85
  type CellRect,
@@ -93,7 +90,13 @@ import { ensureKittySupport, getCachedKittySupport } from "./native/kitty-suppor
93
90
  import { resolveChartRendererState } from "./native/renderer-selection";
94
91
  import { getNativeSurfaceManager } from "./native/surface-manager";
95
92
  import { syncCachedNativeSurface } from "./native/surface-sync";
93
+ import {
94
+ getRenderableCellRect,
95
+ resolveNativeSurfaceVisibleRect,
96
+ type NativeSurfaceRenderableNode,
97
+ } from "./native/surface-visibility";
96
98
  import { formatAxisCell, formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
99
+ import { TimeAxisLabel } from "./time-axis-label";
97
100
 
98
101
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
99
102
  area: "A",
@@ -108,6 +111,7 @@ interface ComparisonStockChartProps {
108
111
  symbols: string[];
109
112
  axisMode: ChartAxisMode;
110
113
  onOpenSymbol: (symbol: string) => void;
114
+ onEditTickers?: () => void;
111
115
  }
112
116
 
113
117
  interface ComparisonChartSymbolSource {
@@ -169,14 +173,6 @@ interface DisplayCursorState {
169
173
  pixelY: number | null;
170
174
  }
171
175
 
172
- interface RenderableNode {
173
- x: number;
174
- y: number;
175
- width: number;
176
- height: number;
177
- parent: RenderableNode | null;
178
- }
179
-
180
176
  const EMPTY_DISPLAY_CURSOR: DisplayCursorState = {
181
177
  cellX: null,
182
178
  cellY: null,
@@ -384,41 +380,6 @@ function resolveCursorMotionKind(
384
380
  : "cell";
385
381
  }
386
382
 
387
- function extractCellRect(renderable: RenderableNode): CellRect {
388
- return {
389
- x: renderable.x,
390
- y: renderable.y,
391
- width: renderable.width,
392
- height: renderable.height,
393
- };
394
- }
395
-
396
- function resolveVisibleRect(
397
- renderable: RenderableNode | null,
398
- terminalWidth: number,
399
- terminalHeight: number,
400
- ): CellRect | null {
401
- if (!renderable) return null;
402
-
403
- let visible: CellRect = {
404
- x: 0,
405
- y: 0,
406
- width: terminalWidth,
407
- height: terminalHeight,
408
- };
409
- let current: RenderableNode | null = renderable;
410
-
411
- while (current) {
412
- const currentRect = extractCellRect(current);
413
- const nextVisible = intersectCellRects(visible, currentRect);
414
- if (!nextVisible) return null;
415
- visible = nextVisible;
416
- current = current.parent;
417
- }
418
-
419
- return visible;
420
- }
421
-
422
383
  function buildBlankPlotLines(width: number, height: number): string[] {
423
384
  return Array.from({ length: height }, () => " ".repeat(width));
424
385
  }
@@ -553,6 +514,7 @@ function ComparisonStockChartView({
553
514
  preferredRenderer,
554
515
  symbolSources,
555
516
  onOpenSymbol,
517
+ onEditTickers,
556
518
  }: ComparisonStockChartViewProps) {
557
519
  const renderer = useNativeRenderer();
558
520
  const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
@@ -587,6 +549,7 @@ function ComparisonStockChartView({
587
549
  const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
588
550
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
589
551
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
552
+ const mouseCrosshairDisabledRef = useRef(false);
590
553
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
591
554
  const animationFrameRef = useRef<number | null>(null);
592
555
  const pendingCanonicalResetRef = useRef(1);
@@ -947,11 +910,10 @@ function ComparisonStockChartView({
947
910
  }
948
911
  return {
949
912
  ...current,
950
- panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
913
+ panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
951
914
  };
952
915
  });
953
916
  }, [
954
- chartWidth,
955
917
  series,
956
918
  seriesDates,
957
919
  viewState.activePreset,
@@ -1021,8 +983,8 @@ function ComparisonStockChartView({
1021
983
  }), [viewState.panOffset, viewState.renderMode, viewState.zoomLevel]);
1022
984
 
1023
985
  const visibleWindow = useMemo(() => (
1024
- getVisibleComparisonWindow(series, projectionViewState, chartWidth)
1025
- ), [chartWidth, projectionViewState, series]);
986
+ getVisibleComparisonWindow(series, projectionViewState)
987
+ ), [projectionViewState, series]);
1026
988
 
1027
989
  const projection = useMemo(() => (
1028
990
  projectComparisonChartData(series, chartWidth, projectionViewState, axisMode)
@@ -1076,6 +1038,14 @@ function ComparisonStockChartView({
1076
1038
  selectedSymbol: viewState.selectedSymbol,
1077
1039
  colors: chartColors,
1078
1040
  }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1041
+ const displayScene = useMemo(() => buildComparisonChartScene(projection, {
1042
+ width: chartWidth,
1043
+ height: chartHeight,
1044
+ cursorX: displayCursorX,
1045
+ cursorY: displayCursorY,
1046
+ selectedSymbol: viewState.selectedSymbol,
1047
+ colors: chartColors,
1048
+ }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
1079
1049
 
1080
1050
  const staticResult = useMemo(() => renderComparisonChart(projection, {
1081
1051
  width: chartWidth,
@@ -1127,6 +1097,12 @@ function ComparisonStockChartView({
1127
1097
  };
1128
1098
 
1129
1099
  return [
1100
+ ...(onEditTickers ? [{
1101
+ id: "tickers",
1102
+ key: "t",
1103
+ label: "ickers",
1104
+ onPress: onEditTickers,
1105
+ }] : []),
1130
1106
  {
1131
1107
  id: "mode",
1132
1108
  key: "m",
@@ -1150,26 +1126,13 @@ function ComparisonStockChartView({
1150
1126
  { id: "reset", key: "0", label: "reset", onPress: resetView },
1151
1127
  ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
1152
1128
  ];
1153
- }, [effectiveResolution, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1129
+ }, [effectiveResolution, onEditTickers, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1154
1130
 
1155
1131
  usePaneFooter("comparison-chart", () => ({
1156
1132
  order: 10,
1157
1133
  hints: footerHints,
1158
1134
  }), [footerHints]);
1159
1135
 
1160
- const liveScene = useMemo(() => (
1161
- effectiveRenderer === "kitty"
1162
- ? staticScene
1163
- : buildComparisonChartScene(projection, {
1164
- width: chartWidth,
1165
- height: chartHeight,
1166
- cursorX: displayCursorX,
1167
- cursorY: displayCursorY,
1168
- selectedSymbol: viewState.selectedSymbol,
1169
- colors: chartColors,
1170
- })
1171
- ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, staticScene, viewState.selectedSymbol]);
1172
-
1173
1136
  const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
1174
1137
  if (displayCursor.cellX === null || displayCursor.cellY === null) return null;
1175
1138
  return {
@@ -1201,8 +1164,9 @@ function ComparisonStockChartView({
1201
1164
 
1202
1165
  const syncPlacement = () => {
1203
1166
  if (effectiveRenderer !== "kitty" || !rendererState.nativeReady || !plotRef.current) return;
1204
- const rect = extractCellRect(plotRef.current);
1205
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1167
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1168
+ const rect = getRenderableCellRect(plot);
1169
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1206
1170
  const previous = lastNativeGeometryRef.current;
1207
1171
  if (previous
1208
1172
  && previous.rect.x === rect.x
@@ -1257,8 +1221,9 @@ function ComparisonStockChartView({
1257
1221
  return;
1258
1222
  }
1259
1223
 
1260
- const plotRect = extractCellRect(plotRef.current);
1261
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1224
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1225
+ const plotRect = getRenderableCellRect(plot);
1226
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1262
1227
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
1263
1228
  const bitmapKey = buildComparisonNativeBitmapKey(
1264
1229
  symbols.length,
@@ -1314,8 +1279,9 @@ function ComparisonStockChartView({
1314
1279
  return;
1315
1280
  }
1316
1281
 
1317
- const plotRect = extractCellRect(plotRef.current);
1318
- const visibleRect = resolveVisibleRect(plotRef.current, renderer.terminalWidth, renderer.terminalHeight);
1282
+ const plot = plotRef.current as NativeSurfaceRenderableNode;
1283
+ const plotRect = getRenderableCellRect(plot);
1284
+ const visibleRect = resolveNativeSurfaceVisibleRect(plot, renderer.terminalWidth, renderer.terminalHeight);
1319
1285
  const bitmapSize = computeBitmapSize(plotRect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
1320
1286
  const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
1321
1287
  const overlayPixelX = scaleLocalPixelCoordinate(
@@ -1421,7 +1387,7 @@ function ComparisonStockChartView({
1421
1387
  return;
1422
1388
  case "a": {
1423
1389
  const panStep = getKeyboardPanCellCount(chartWidth);
1424
- if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1390
+ if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1425
1391
  kind: "pan-left",
1426
1392
  targetPanOffset: viewState.panOffset + panStep,
1427
1393
  })) {
@@ -1429,7 +1395,7 @@ function ComparisonStockChartView({
1429
1395
  }
1430
1396
  setViewState((current) => ({
1431
1397
  ...clearActivePreset(current),
1432
- panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1398
+ panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1433
1399
  }));
1434
1400
  return;
1435
1401
  }
@@ -1437,7 +1403,7 @@ function ComparisonStockChartView({
1437
1403
  const panStep = getKeyboardPanCellCount(chartWidth);
1438
1404
  setViewState((current) => ({
1439
1405
  ...clearActivePreset(current),
1440
- panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1406
+ panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1441
1407
  }));
1442
1408
  return;
1443
1409
  }
@@ -1453,7 +1419,11 @@ function ComparisonStockChartView({
1453
1419
  setResolution(nextResolution);
1454
1420
  return;
1455
1421
  }
1422
+ case "t":
1423
+ onEditTickers?.();
1424
+ return;
1456
1425
  case "h":
1426
+ mouseCrosshairDisabledRef.current = false;
1457
1427
  cursorMotionKindRef.current = "discrete";
1458
1428
  setViewState((current) => ({
1459
1429
  ...current,
@@ -1461,6 +1431,7 @@ function ComparisonStockChartView({
1461
1431
  }));
1462
1432
  return;
1463
1433
  case "l":
1434
+ mouseCrosshairDisabledRef.current = false;
1464
1435
  cursorMotionKindRef.current = "discrete";
1465
1436
  setViewState((current) => ({
1466
1437
  ...current,
@@ -1468,6 +1439,7 @@ function ComparisonStockChartView({
1468
1439
  }));
1469
1440
  return;
1470
1441
  case "escape":
1442
+ mouseCrosshairDisabledRef.current = true;
1471
1443
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1472
1444
  setViewState((current) => ({ ...current, cursorX: null, cursorY: null }));
1473
1445
  return;
@@ -1524,6 +1496,7 @@ function ComparisonStockChartView({
1524
1496
  });
1525
1497
 
1526
1498
  const handlePlotMove = (event: ChartMouseEvent) => {
1499
+ if (mouseCrosshairDisabledRef.current) return;
1527
1500
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1528
1501
  if (!localPointer) return;
1529
1502
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1542,6 +1515,7 @@ function ComparisonStockChartView({
1542
1515
  };
1543
1516
 
1544
1517
  const handlePlotDown = (event: ChartMouseEvent) => {
1518
+ mouseCrosshairDisabledRef.current = false;
1545
1519
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1546
1520
  if (!localPointer) return;
1547
1521
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1564,6 +1538,7 @@ function ComparisonStockChartView({
1564
1538
  };
1565
1539
 
1566
1540
  const handlePlotDrag = (event: ChartMouseEvent) => {
1541
+ if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
1567
1542
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1568
1543
  if (localPointer) {
1569
1544
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1628,7 +1603,7 @@ function ComparisonStockChartView({
1628
1603
  if (scrollPanCells === 0) return;
1629
1604
 
1630
1605
  const targetPanOffset = viewState.panOffset + scrollPanCells;
1631
- if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1606
+ if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1632
1607
  kind: "pan-left",
1633
1608
  targetPanOffset,
1634
1609
  })) {
@@ -1639,7 +1614,7 @@ function ComparisonStockChartView({
1639
1614
  panOffset: clamp(
1640
1615
  current.panOffset + scrollPanCells,
1641
1616
  0,
1642
- getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth),
1617
+ getMaxComparisonPanOffset(series, current.zoomLevel),
1643
1618
  ),
1644
1619
  }));
1645
1620
  };
@@ -1649,7 +1624,8 @@ function ComparisonStockChartView({
1649
1624
  }
1650
1625
 
1651
1626
  const hasChartData = series.some((entry) => entry.points.length > 0);
1652
- const cursorScene = effectiveRenderer === "kitty" ? staticScene : liveScene;
1627
+ const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
1628
+ const cursorScene = hasDisplayCursor ? displayScene : staticScene;
1653
1629
  const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
1654
1630
  const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
1655
1631
  const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
@@ -1677,6 +1653,8 @@ function ComparisonStockChartView({
1677
1653
  start: visibleWindow.dates[0] ?? null,
1678
1654
  end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
1679
1655
  });
1656
+ const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
1657
+ const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.activeDate ?? null : null;
1680
1658
 
1681
1659
  const canvasBitmapSize = useMemo(() => {
1682
1660
  if (!canvasCharts) return null;
@@ -1965,7 +1943,14 @@ function ComparisonStockChartView({
1965
1943
  </Box>
1966
1944
 
1967
1945
  <Box height={timeAxisRows}>
1968
- <Text fg={colors.textDim}>{timeAxisLabel}</Text>
1946
+ <TimeAxisLabel
1947
+ timeLabels={timeAxisLabel}
1948
+ width={chartWidth}
1949
+ cursorColumn={cursorTimeAxisColumn}
1950
+ cursorDate={cursorTimeAxisDate}
1951
+ dates={projection.dates}
1952
+ cursorColor={chartColors.crosshairColor}
1953
+ />
1969
1954
  </Box>
1970
1955
 
1971
1956
  {legendRows > 0 && (
@@ -17,6 +17,7 @@ import { chunkBase64Payload, encodeKittyTransmitRgba } from "./kitty-protocol";
17
17
  import { resolveChartRendererState } from "./renderer-selection";
18
18
  import { computeSurfaceVisibleFragments, NativeSurfaceManager } from "./surface-manager";
19
19
  import { syncCachedNativeSurface } from "./surface-sync";
20
+ import { resolveNativeSurfaceVisibleRect, type NativeSurfaceRenderableNode } from "./surface-visibility";
20
21
 
21
22
  describe("resolveChartRendererState", () => {
22
23
  test("resolves auto and forced kitty correctly", () => {
@@ -98,6 +99,28 @@ describe("computeNativePlacement", () => {
98
99
  });
99
100
  });
100
101
 
102
+ describe("resolveNativeSurfaceVisibleRect", () => {
103
+ test("treats hidden ancestors as invisible for native graphics", () => {
104
+ const hiddenParent: NativeSurfaceRenderableNode = {
105
+ x: 0,
106
+ y: 0,
107
+ width: 80,
108
+ height: 20,
109
+ visible: false,
110
+ parent: null,
111
+ };
112
+ const child: NativeSurfaceRenderableNode = {
113
+ x: 2,
114
+ y: 3,
115
+ width: 20,
116
+ height: 8,
117
+ parent: hiddenParent,
118
+ };
119
+
120
+ expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toBeNull();
121
+ });
122
+ });
123
+
101
124
  describe("renderNativeChartBase", () => {
102
125
  test("leaves untouched bitmap pixels transparent", () => {
103
126
  const palette = resolveChartPalette({
@@ -0,0 +1,48 @@
1
+ import { intersectCellRects, type CellRect } from "./chart-rasterizer";
2
+
3
+ export interface NativeSurfaceRenderableNode {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ visible?: boolean;
9
+ parent: NativeSurfaceRenderableNode | null;
10
+ }
11
+
12
+ export function getRenderableCellRect(
13
+ renderable: Pick<NativeSurfaceRenderableNode, "x" | "y" | "width" | "height">,
14
+ ): CellRect {
15
+ return {
16
+ x: renderable.x,
17
+ y: renderable.y,
18
+ width: renderable.width,
19
+ height: renderable.height,
20
+ };
21
+ }
22
+
23
+ export function resolveNativeSurfaceVisibleRect(
24
+ renderable: NativeSurfaceRenderableNode | null,
25
+ terminalWidth: number,
26
+ terminalHeight: number,
27
+ ): CellRect | null {
28
+ if (!renderable) return null;
29
+
30
+ let visible: CellRect = {
31
+ x: 0,
32
+ y: 0,
33
+ width: terminalWidth,
34
+ height: terminalHeight,
35
+ };
36
+ let current: NativeSurfaceRenderableNode | null = renderable;
37
+
38
+ while (current) {
39
+ if (current.visible === false) return null;
40
+
41
+ const nextVisible = intersectCellRects(visible, getRenderableCellRect(current));
42
+ if (!nextVisible) return null;
43
+ visible = nextVisible;
44
+ current = current.parent;
45
+ }
46
+
47
+ return visible;
48
+ }
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { Box, ChartSurface, Text, useUiCapabilities } from "../../ui";
2
+ import { Box, ChartSurface, Text, useNativeRenderer, useUiCapabilities } from "../../ui";
3
3
  import type { ProjectedChartPoint } from "./chart-data";
4
4
  import {
5
5
  buildChartScene,
@@ -8,7 +8,7 @@ import {
8
8
  renderChart,
9
9
  type RenderChartOptions,
10
10
  } from "./chart-renderer";
11
- import { renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
11
+ import { computeBitmapSize, renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
12
12
 
13
13
  export interface StaticChartSurfaceProps extends Omit<
14
14
  RenderChartOptions,
@@ -47,10 +47,12 @@ export function StaticChartSurface({
47
47
  }: StaticChartSurfaceProps) {
48
48
  const {
49
49
  canvasCharts,
50
+ nativeCharts,
50
51
  cellWidthPx = 8,
51
52
  cellHeightPx = 18,
52
53
  pixelRatio = 1,
53
54
  } = useUiCapabilities();
55
+ const nativeRenderer = useNativeRenderer();
54
56
  const timeAxisRows = showTimeAxis ? 1 : 0;
55
57
  const labelRows = yAxisLabel ? 1 : 0;
56
58
  const totalWidth = Math.max(1, Math.floor(width));
@@ -101,14 +103,36 @@ export function StaticChartSurface({
101
103
  : 0;
102
104
  const axisGap = axisWidth > 0 ? 1 : 0;
103
105
  const plotWidth = Math.max(1, totalWidth - axisWidth - axisGap);
106
+ const rendererResolution = nativeRenderer.resolution;
107
+ const rendererTerminalWidth = nativeRenderer.terminalWidth;
108
+ const rendererTerminalHeight = nativeRenderer.terminalHeight;
104
109
  const bitmapSize = useMemo(() => {
105
- if (!canvasCharts) return null;
110
+ if (nativeCharts && rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0) {
111
+ return computeBitmapSize(
112
+ { x: 0, y: 0, width: plotWidth, height: plotHeight },
113
+ rendererResolution,
114
+ rendererTerminalWidth,
115
+ rendererTerminalHeight,
116
+ );
117
+ }
118
+ if (!canvasCharts && !nativeCharts) return null;
106
119
  const scale = Math.max(1, pixelRatio);
107
120
  return {
108
121
  pixelWidth: Math.max(1, Math.round(plotWidth * cellWidthPx * scale)),
109
122
  pixelHeight: Math.max(1, Math.round(plotHeight * cellHeightPx * scale)),
110
123
  };
111
- }, [canvasCharts, cellHeightPx, cellWidthPx, pixelRatio, plotHeight, plotWidth]);
124
+ }, [
125
+ canvasCharts,
126
+ cellHeightPx,
127
+ cellWidthPx,
128
+ nativeCharts,
129
+ pixelRatio,
130
+ plotHeight,
131
+ plotWidth,
132
+ rendererResolution,
133
+ rendererTerminalHeight,
134
+ rendererTerminalWidth,
135
+ ]);
112
136
  const renderOptions = useMemo<RenderChartOptions>(() => ({
113
137
  width: plotWidth,
114
138
  height: plotHeight,
@@ -9,7 +9,6 @@ import {
9
9
  resolveAdjacentSelectionCursorX,
10
10
  resolveChartKeyboardKey,
11
11
  resolveIndicatorBufferRange,
12
- resolveVisibleChartDateWindow,
13
12
  resolveSelectionDisplayCursorState,
14
13
  } from "./stock-chart";
15
14
  import { getPointTerminalColumn } from "./chart-renderer";
@@ -215,21 +214,6 @@ describe("stock chart auto helpers", () => {
215
214
  expect(nextState.window).toEqual(renderedAutoView.window);
216
215
  });
217
216
 
218
- test("derives the visible label window from the actual visible chart points", () => {
219
- const visibleWindow = resolveVisibleChartDateWindow([
220
- { date: new Date("2026-01-03T00:00:00Z") },
221
- { date: new Date("2026-01-06T00:00:00Z") },
222
- ], {
223
- start: new Date("2026-01-01T00:00:00Z"),
224
- end: new Date("2026-01-08T00:00:00Z"),
225
- });
226
-
227
- expect(visibleWindow).toEqual({
228
- start: new Date("2026-01-03T00:00:00Z"),
229
- end: new Date("2026-01-06T00:00:00Z"),
230
- });
231
- });
232
-
233
217
  test("zooms in by at least one visible point on every auto step when more detail is available", () => {
234
218
  const history = makeDailyHistory(1, 10);
235
219