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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Bloomberg-style stock portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -53,6 +53,7 @@
53
53
  "@opentui/react": "^0.1.90",
54
54
  "@stoqey/ib": "^1.5.3",
55
55
  "@tanstack/react-virtual": "^3.13.23",
56
+ "jimp": "1.6.0",
56
57
  "opentui-spinner": "^0.0.6",
57
58
  "react": "19.2.5",
58
59
  "rxjs": "^7.8.2"
@@ -1,5 +1,5 @@
1
1
  diff --git a/index-e89anq5x.js b/index-e89anq5x.js
2
- index e3b6cf9..14a46d7 100644
2
+ index e3b6cf9f90b13698966eec991fa7a1b2e1e22d96..709940f3998e5cb1caa88642fa68fa9ace80b7b1 100644
3
3
  --- a/index-e89anq5x.js
4
4
  +++ b/index-e89anq5x.js
5
5
  @@ -6328,17 +6328,27 @@ class MouseParser {
@@ -66,16 +66,39 @@ index e3b6cf9..14a46d7 100644
66
66
  consumed: index - offset + 1
67
67
  };
68
68
  }
69
- @@ -6408,7 +6418,7 @@ class MouseParser {
69
+ @@ -6408,10 +6418,12 @@ class MouseParser {
70
70
  consumed: 6
71
71
  };
72
72
  }
73
73
  - decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease) {
74
+ - const button = rawButtonCode & 3;
74
75
  + decodeSgrEvent(rawButtonCode, wireX, wireY, pressRelease, context = void 0) {
75
- const button = rawButtonCode & 3;
76
+ + const encodedButton = rawButtonCode & 3;
77
+ + const isExtendedButton = (rawButtonCode & 128) !== 0;
78
+ + const button = isExtendedButton ? 8 + encodedButton : encodedButton;
76
79
  const isScroll = (rawButtonCode & 64) !== 0;
77
- const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button];
78
- @@ -6443,11 +6453,26 @@ class MouseParser {
80
+ - const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button];
81
+ + const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[encodedButton];
82
+ const isMotion = (rawButtonCode & 32) !== 0;
83
+ const modifiers = {
84
+ shift: (rawButtonCode & 4) !== 0,
85
+ @@ -6422,7 +6434,7 @@ class MouseParser {
86
+ let scrollInfo;
87
+ if (isMotion) {
88
+ const isDragging = this.mouseButtonsPressed.size > 0;
89
+ - if (button === 3) {
90
+ + if (!isExtendedButton && encodedButton === 3) {
91
+ type = "move";
92
+ } else if (isDragging) {
93
+ type = "drag";
94
+ @@ -6437,17 +6449,32 @@ class MouseParser {
95
+ };
96
+ } else {
97
+ type = pressRelease === "M" ? "down" : "up";
98
+ - if (type === "down" && button !== 3) {
99
+ + if (type === "down" && (isExtendedButton || encodedButton !== 3)) {
100
+ this.mouseButtonsPressed.add(button);
101
+ } else if (type === "up") {
79
102
  this.mouseButtonsPressed.clear();
80
103
  }
81
104
  }
@@ -90,9 +113,10 @@ index e3b6cf9..14a46d7 100644
90
113
  + const pixelY = pixelModeDetected ? Math.max(wireY - 1, 0) : void 0;
91
114
  return {
92
115
  type,
93
- button: button === 3 ? 0 : button,
116
+ - button: button === 3 ? 0 : button,
94
117
  - x: wireX - 1,
95
118
  - y: wireY - 1,
119
+ + button: !isExtendedButton && encodedButton === 3 ? 0 : button,
96
120
  + x: pixelModeDetected && pixelX !== void 0
97
121
  + ? this.projectPixelToCell(pixelX, context?.pixelWidth ?? 0, context?.terminalWidth ?? 0)
98
122
  + : wireX - 1,
@@ -104,7 +128,7 @@ index e3b6cf9..14a46d7 100644
104
128
  modifiers,
105
129
  scroll: scrollInfo
106
130
  };
107
- @@ -6856,7 +6881,13 @@ var DEFAULT_PROTOCOL_CONTEXT = {
131
+ @@ -6856,7 +6883,13 @@ var DEFAULT_PROTOCOL_CONTEXT = {
108
132
  kittyKeyboardEnabled: false,
109
133
  privateCapabilityRepliesActive: false,
110
134
  pixelResolutionQueryActive: false,
@@ -119,7 +143,7 @@ index e3b6cf9..14a46d7 100644
119
143
  };
120
144
  var RXVT_DOLLAR_CSI_RE = /^\x1b\[\d+\$$/;
121
145
  var SYSTEM_CLOCK = new SystemClock;
122
- @@ -7149,7 +7180,13 @@ class StdinParser {
146
+ @@ -7149,7 +7182,13 @@ class StdinParser {
123
147
  kittyKeyboardEnabled: options.protocolContext?.kittyKeyboardEnabled ?? false,
124
148
  privateCapabilityRepliesActive: options.protocolContext?.privateCapabilityRepliesActive ?? false,
125
149
  pixelResolutionQueryActive: options.protocolContext?.pixelResolutionQueryActive ?? false,
@@ -134,7 +158,7 @@ index e3b6cf9..14a46d7 100644
134
158
  };
135
159
  }
136
160
  get bufferCapacity() {
137
- @@ -7952,11 +7989,14 @@ class StdinParser {
161
+ @@ -7952,11 +7991,14 @@ class StdinParser {
138
162
  });
139
163
  }
140
164
  emitMouse(rawBytes, encoding) {
@@ -150,7 +174,7 @@ index e3b6cf9..14a46d7 100644
150
174
  this.events.push({
151
175
  type: "mouse",
152
176
  raw: decodeLatin1(rawBytes),
153
- @@ -17295,6 +17335,8 @@ class MouseEvent {
177
+ @@ -17295,6 +17337,8 @@ class MouseEvent {
154
178
  button;
155
179
  x;
156
180
  y;
@@ -159,7 +183,7 @@ index e3b6cf9..14a46d7 100644
159
183
  source;
160
184
  modifiers;
161
185
  scroll;
162
- @@ -17314,6 +17356,8 @@ class MouseEvent {
186
+ @@ -17314,6 +17358,8 @@ class MouseEvent {
163
187
  this.button = attributes.button;
164
188
  this.x = attributes.x;
165
189
  this.y = attributes.y;
@@ -168,7 +192,7 @@ index e3b6cf9..14a46d7 100644
168
192
  this.modifiers = attributes.modifiers;
169
193
  this.scroll = attributes.scroll;
170
194
  this.source = attributes.source;
171
- @@ -17479,6 +17523,8 @@ class CliRenderer extends EventEmitter8 {
195
+ @@ -17479,6 +17525,8 @@ class CliRenderer extends EventEmitter8 {
172
196
  resizeDebounceDelay = 100;
173
197
  enableMouseMovement = false;
174
198
  _useMouse = true;
@@ -177,7 +201,7 @@ index e3b6cf9..14a46d7 100644
177
201
  autoFocus = true;
178
202
  _useAlternateScreen = env.OTUI_USE_ALTERNATE_SCREEN;
179
203
  _suspendedMouseEnabled = false;
180
- @@ -17507,7 +17553,7 @@ class CliRenderer extends EventEmitter8 {
204
+ @@ -17507,7 +17555,7 @@ class CliRenderer extends EventEmitter8 {
181
205
  this.handleResize(width, height);
182
206
  }).bind(this);
183
207
  _capabilities = null;
@@ -186,7 +210,7 @@ index e3b6cf9..14a46d7 100644
186
210
  _hasPointer = false;
187
211
  _lastPointerModifiers = { shift: false, alt: false, ctrl: false };
188
212
  _currentMousePointerStyle = undefined;
189
- @@ -17661,7 +17707,13 @@ Captured output:
213
+ @@ -17661,7 +17709,13 @@ Captured output:
190
214
  kittyKeyboardEnabled: useKittyForParsing,
191
215
  privateCapabilityRepliesActive: false,
192
216
  pixelResolutionQueryActive: false,
@@ -201,7 +225,7 @@ index e3b6cf9..14a46d7 100644
201
225
  },
202
226
  clock: this.clock
203
227
  });
204
- @@ -17983,12 +18035,14 @@ Captured output:
228
+ @@ -17983,12 +18037,14 @@ Captured output:
205
229
  enableMouse() {
206
230
  this._useMouse = true;
207
231
  this.lib.enableMouse(this.rendererPtr, this.enableMouseMovement);
@@ -216,7 +240,7 @@ index e3b6cf9..14a46d7 100644
216
240
  }
217
241
  enableKittyKeyboard(flags = 3) {
218
242
  this.lib.enableKittyKeyboard(this.rendererPtr, flags);
219
- @@ -17998,6 +18052,64 @@ Captured output:
243
+ @@ -17998,6 +18054,64 @@ Captured output:
220
244
  this.lib.disableKittyKeyboard(this.rendererPtr);
221
245
  this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: false }, true);
222
246
  }
@@ -281,7 +305,7 @@ index e3b6cf9..14a46d7 100644
281
305
  set useThread(useThread) {
282
306
  this._useThread = useThread;
283
307
  this.lib.setUseThread(this.rendererPtr, useThread);
284
- @@ -18031,6 +18143,7 @@ Captured output:
308
+ @@ -18031,6 +18145,7 @@ Captured output:
285
309
  if (this._useMouse) {
286
310
  this.enableMouse();
287
311
  }
@@ -289,7 +313,7 @@ index e3b6cf9..14a46d7 100644
289
313
  this.queryPixelResolution();
290
314
  }
291
315
  stdinListener = ((chunk) => {
292
- @@ -18070,6 +18183,7 @@ Captured output:
316
+ @@ -18070,6 +18185,7 @@ Captured output:
293
317
  if (isCapabilityResponse(sequence)) {
294
318
  this.lib.processCapabilityResponse(this.rendererPtr, sequence);
295
319
  this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr);
@@ -297,7 +321,7 @@ index e3b6cf9..14a46d7 100644
297
321
  this.emit("capabilities" /* CAPABILITIES */, this._capabilities);
298
322
  return true;
299
323
  }
300
- @@ -18079,6 +18193,7 @@ Captured output:
324
+ @@ -18079,6 +18195,7 @@ Captured output:
301
325
  if (sequence === "\x1B[I") {
302
326
  if (this.shouldRestoreModesOnNextFocus) {
303
327
  this.lib.restoreTerminalModes(this.rendererPtr);
@@ -305,7 +329,7 @@ index e3b6cf9..14a46d7 100644
305
329
  this.shouldRestoreModesOnNextFocus = false;
306
330
  }
307
331
  if (this._terminalFocusState !== true) {
308
- @@ -18187,6 +18302,7 @@ Captured output:
332
+ @@ -18187,6 +18304,7 @@ Captured output:
309
333
  }
310
334
  this.waitingForPixelResolution = false;
311
335
  this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: false }, true);
@@ -313,7 +337,7 @@ index e3b6cf9..14a46d7 100644
313
337
  return true;
314
338
  }
315
339
  return false;
316
- @@ -18221,9 +18337,15 @@ Captured output:
340
+ @@ -18221,9 +18339,15 @@ Captured output:
317
341
  return false;
318
342
  }
319
343
  mouseEvent.y -= this.renderOffset;
@@ -329,7 +353,7 @@ index e3b6cf9..14a46d7 100644
329
353
  this._hasPointer = true;
330
354
  this._lastPointerModifiers = mouseEvent.modifiers;
331
355
  if (this._console.visible) {
332
- @@ -18352,6 +18474,8 @@ Captured output:
356
+ @@ -18352,6 +18476,8 @@ Captured output:
333
357
  button: 0,
334
358
  x: this._latestPointer.x,
335
359
  y: this._latestPointer.y,
@@ -338,7 +362,7 @@ index e3b6cf9..14a46d7 100644
338
362
  modifiers: this._lastPointerModifiers
339
363
  };
340
364
  if (lastOver && !lastOver.isDestroyed) {
341
- @@ -18435,6 +18559,7 @@ Captured output:
365
+ @@ -18435,6 +18561,7 @@ Captured output:
342
366
  const prevWidth = this._terminalWidth;
343
367
  this._terminalWidth = width;
344
368
  this._terminalHeight = height;
@@ -346,7 +370,7 @@ index e3b6cf9..14a46d7 100644
346
370
  this.queryPixelResolution();
347
371
  this.setCapturedRenderable(undefined);
348
372
  this.stdinParser?.resetMouseState();
349
- @@ -18712,6 +18837,7 @@ Captured output:
373
+ @@ -18712,6 +18839,7 @@ Captured output:
350
374
  }
351
375
  this._isRunning = false;
352
376
  this.waitingForPixelResolution = false;
package/src/app.tsx CHANGED
@@ -17,6 +17,7 @@ import { Header } from "./components/layout/header";
17
17
  import { StatusBar } from "./components/layout/status-bar";
18
18
  import { Shell } from "./components/layout/shell";
19
19
  import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
20
+ import { getVisiblePaneCycleOrder } from "./components/layout/pane-cycle-order";
20
21
  import { CommandBar } from "./components/command-bar/command-bar";
21
22
  import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
22
23
  import { useDialog, useDialogState } from "./ui/dialog";
@@ -1441,18 +1442,20 @@ function AppInner({
1441
1442
  if (state.commandBarOpen || state.inputCaptured) return;
1442
1443
 
1443
1444
  if (event.name === "tab") {
1444
- // Build pane order from current layout for cycling
1445
- const layout = state.config.layout;
1446
- const paneOrder = [
1447
- ...getDockedPaneIds(layout),
1448
- ...layout.floating.map((entry) => entry.instanceId),
1449
- ];
1445
+ const paneOrder = getVisiblePaneCycleOrder(
1446
+ state.config.layout,
1447
+ pluginRegistry,
1448
+ state.config.disabledPlugins,
1449
+ );
1450
+ if (paneOrder.length === 0) return;
1450
1451
 
1451
1452
  if (event.shift) {
1452
1453
  dispatch({ type: "FOCUS_PREV", paneOrder });
1453
1454
  } else {
1454
1455
  dispatch({ type: "FOCUS_NEXT", paneOrder });
1455
1456
  }
1457
+ event.preventDefault();
1458
+ event.stopPropagation();
1456
1459
  } else if (!isDetachedWindow && event.name === "q" && !(focusedPane?.paneId === "ticker-detail" && focusedDetailTab === "financials")) {
1457
1460
  rendererHost.requestExit();
1458
1461
  } else if (event.name === "r") {
@@ -4,7 +4,6 @@ import { join } from "path";
4
4
  import { tmpdir } from "os";
5
5
  import { createDefaultConfig } from "../types/config";
6
6
  import type { GloomPlugin } from "../types/plugin";
7
- import type { LoadedExternalPlugin } from "../plugins/loader";
8
7
  import {
9
8
  buildCliCommandRegistry,
10
9
  createCliCommandContext,
@@ -13,7 +13,6 @@ import {
13
13
  isDateWindowWithinTimeRange,
14
14
  isTimeRangeAtOrBelow,
15
15
  subtractTimeRange,
16
- type ChartResolutionSupport,
17
16
  type ManualChartResolution,
18
17
  } from "./chart-resolution";
19
18
 
@@ -1,6 +1,5 @@
1
1
  import type { PricePoint } from "../../types/financials";
2
- import type { ChartRenderMode, ChartViewState, VisibleWindow } from "./chart-types";
3
- import { getVisiblePointCount } from "./chart-viewport";
2
+ import type { ChartRenderMode } from "./chart-types";
4
3
 
5
4
  export interface ProjectedChartPoint {
6
5
  date: Date;
@@ -76,34 +75,6 @@ export function resolveRenderMode(
76
75
  };
77
76
  }
78
77
 
79
- /**
80
- * Apply zoom and pan to get the visible data window.
81
- */
82
- export function getVisibleWindow(
83
- history: PricePoint[],
84
- viewState: ChartViewState,
85
- chartWidth: number,
86
- ): VisibleWindow {
87
- if (history.length === 0) {
88
- return { points: [], startIdx: 0, endIdx: 0 };
89
- }
90
-
91
- const visibleCount = getVisiblePointCount(history.length, viewState.zoomLevel);
92
-
93
- // Clamp pan offset
94
- const maxPan = Math.max(history.length - visibleCount, 0);
95
- const pan = Math.min(Math.max(viewState.panOffset, 0), maxPan);
96
-
97
- const endIdx = history.length - pan;
98
- const startIdx = Math.max(endIdx - visibleCount, 0);
99
-
100
- return {
101
- points: history.slice(startIdx, endIdx),
102
- startIdx,
103
- endIdx,
104
- };
105
- }
106
-
107
78
  function normalizePoint(point: PricePoint): ProjectedChartPoint {
108
79
  const open = point.open ?? point.close;
109
80
  const high = point.high ?? point.close;
@@ -334,7 +305,6 @@ export function bucketOhlcSeries(
334
305
  return result;
335
306
  }
336
307
 
337
- const MS_HOUR = 3600_000;
338
308
  const MS_DAY = 86400_000;
339
309
 
340
310
  /**
@@ -918,6 +918,76 @@ function writeAxisLabel(axis: string[], start: number, label: string) {
918
918
  }
919
919
  }
920
920
 
921
+ export interface AxisLabelSegment {
922
+ text: string;
923
+ highlighted: boolean;
924
+ }
925
+
926
+ function formatCursorTimeAxisValue(
927
+ value: Date | string | number,
928
+ dates: Array<Date | string | number>,
929
+ width: number,
930
+ ): string {
931
+ const date = value instanceof Date ? value : new Date(value);
932
+ if (isNaN(date.getTime())) return "—";
933
+
934
+ const normalizedDates = dates.map((entry) => (entry instanceof Date ? entry : new Date(entry)));
935
+ const validDates = normalizedDates.filter((entry) => !isNaN(entry.getTime()));
936
+ if (validDates.length === 0) return formatDateShort(date);
937
+
938
+ const first = validDates[0]!;
939
+ const last = validDates[validDates.length - 1]!;
940
+ const spanMs = Math.max(last.getTime() - first.getTime(), 1);
941
+ const roughLabelCount = Math.max(Math.floor(normalizeCount(width, 0) / 10), 2);
942
+ const minGapMs = getMinPositiveGapMs(validDates);
943
+ const effectiveStepMs = Math.max(spanMs / Math.max(roughLabelCount - 1, 1), minGapMs || 0);
944
+ const unit = resolveAxisLabelUnit(effectiveStepMs);
945
+ const counterpart = isSameCalendarDay(first, last)
946
+ ? first
947
+ : isSameCalendarDay(date, first)
948
+ ? last
949
+ : first;
950
+
951
+ return formatTimeAxisBoundaryLabel(date, unit, counterpart);
952
+ }
953
+
954
+ export function buildCursorTimeAxisSegments({
955
+ timeLabels,
956
+ width,
957
+ cursorColumn,
958
+ cursorDate,
959
+ dates,
960
+ }: {
961
+ timeLabels: string;
962
+ width: number;
963
+ cursorColumn: number | null;
964
+ cursorDate: Date | string | number | null;
965
+ dates: Array<Date | string | number>;
966
+ }): AxisLabelSegment[] {
967
+ const axisWidth = normalizeCount(width, 0);
968
+ if (axisWidth <= 0) return [];
969
+
970
+ const base = timeLabels.padEnd(axisWidth).slice(0, axisWidth);
971
+ if (cursorColumn === null || cursorDate === null) {
972
+ return [{ text: base, highlighted: false }];
973
+ }
974
+
975
+ const label = formatCursorTimeAxisValue(cursorDate, dates, axisWidth);
976
+ if (!label) return [{ text: base, highlighted: false }];
977
+
978
+ const axis = base.split("");
979
+ const column = Math.max(Math.min(Math.round(cursorColumn), axisWidth - 1), 0);
980
+ const start = resolveAxisLabelStart(column, label, axisWidth);
981
+ const end = Math.min(start + label.length, axisWidth);
982
+ writeAxisLabel(axis, start, label);
983
+
984
+ return [
985
+ { text: axis.slice(0, start).join(""), highlighted: false },
986
+ { text: axis.slice(start, end).join(""), highlighted: true },
987
+ { text: axis.slice(end).join(""), highlighted: false },
988
+ ].filter((segment) => segment.text.length > 0);
989
+ }
990
+
921
991
  export function buildTimeAxis(dates: Array<Date | string | number>, width: number): string {
922
992
  const axisWidth = normalizeCount(width, 0);
923
993
  if (dates.length === 0 || axisWidth <= 0) return "";
@@ -3,10 +3,8 @@ import {
3
3
  CHART_RESOLUTION_STEP_MS,
4
4
  clampTimeRangeForResolution,
5
5
  getActiveRangePreset,
6
- getBestSupportedResolutionForDateWindow,
7
6
  getBestSupportedResolutionForVisibleWindow,
8
7
  getPresetResolution,
9
- intersectChartResolutions,
10
8
  isRangePresetSupported,
11
9
  normalizeChartResolution,
12
10
  sortChartResolutions,
@@ -30,14 +28,6 @@ describe("chart-resolution", () => {
30
28
  expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
31
29
  });
32
30
 
33
- test("intersects and sorts provider capabilities", () => {
34
- expect(intersectChartResolutions([
35
- ["1d", "5m", "1wk", "nope"],
36
- ["1wk", "1d", "1mo"],
37
- ["1d", "1wk"],
38
- ])).toEqual(["1d", "1wk"]);
39
- });
40
-
41
31
  test("checks whether a range preset is supported by the visible capability set", () => {
42
32
  expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
43
33
  expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
@@ -57,32 +47,6 @@ describe("chart-resolution", () => {
57
47
  expect(sortChartResolutions(["1wk", "auto", "15m", "1d"])).toEqual(["auto", "15m", "1d", "1wk"]);
58
48
  });
59
49
 
60
- test("picks the best supported resolution for the current visible date window", () => {
61
- const support = [
62
- { resolution: "1m", maxRange: "1D" },
63
- { resolution: "5m", maxRange: "1W" },
64
- { resolution: "15m", maxRange: "1M" },
65
- { resolution: "1h", maxRange: "3M" },
66
- { resolution: "1d", maxRange: "5Y" },
67
- { resolution: "1wk", maxRange: "ALL" },
68
- ] as const;
69
-
70
- expect(getBestSupportedResolutionForDateWindow({
71
- start: new Date("2026-01-08T10:00:00Z"),
72
- end: new Date("2026-01-08T16:00:00Z"),
73
- }, support)).toBe("1m");
74
-
75
- expect(getBestSupportedResolutionForDateWindow({
76
- start: new Date("2026-01-01T00:00:00Z"),
77
- end: new Date("2026-01-08T00:00:00Z"),
78
- }, support)).toBe("5m");
79
-
80
- expect(getBestSupportedResolutionForDateWindow({
81
- start: new Date("2021-01-01T00:00:00Z"),
82
- end: new Date("2026-01-01T00:00:00Z"),
83
- }, support)).toBe("1wk");
84
- });
85
-
86
50
  test("picks a denser supported resolution as the visible window narrows", () => {
87
51
  const support = [
88
52
  { resolution: "1m", maxRange: "1D" },
@@ -151,24 +151,6 @@ export function sortChartResolutions<T extends ChartResolution>(resolutions: rea
151
151
  ));
152
152
  }
153
153
 
154
- function normalizeManualChartResolutions(resolutions: readonly string[]): ManualChartResolution[] {
155
- const seen = new Set<ManualChartResolution>();
156
- for (const resolution of resolutions) {
157
- if (isManualChartResolution(resolution)) {
158
- seen.add(resolution);
159
- }
160
- }
161
- return sortChartResolutions([...seen]);
162
- }
163
-
164
- export function intersectChartResolutions(resolutionSets: Array<readonly string[]>): ManualChartResolution[] {
165
- if (resolutionSets.length === 0) return [];
166
- const [first = new Set<ManualChartResolution>(), ...rest] = resolutionSets.map((set) => new Set(normalizeManualChartResolutions(set)));
167
- return sortChartResolutions(
168
- [...first].filter((resolution) => rest.every((set) => set.has(resolution))),
169
- );
170
- }
171
-
172
154
  function sortChartResolutionSupport(support: readonly ChartResolutionSupport[]): ChartResolutionSupport[] {
173
155
  return [...support].sort((left, right) => (
174
156
  CHART_RESOLUTION_ORDER.indexOf(left.resolution) - CHART_RESOLUTION_ORDER.indexOf(right.resolution)
@@ -324,15 +306,6 @@ export function getBestSupportedResolutionForPreset(
324
306
  return supportedResolutions[supportedResolutions.length - 1] ?? null;
325
307
  }
326
308
 
327
- export function getBestSupportedResolutionForDateWindow(
328
- window: { start: Date | null; end: Date | null } | null,
329
- support: readonly ChartResolutionSupport[] | ReadonlyMap<ManualChartResolution, TimeRange>,
330
- ): ManualChartResolution | null {
331
- if (!window?.start || !window.end) return null;
332
- const range = getTimeRangeForDateWindow(window);
333
- return getBestSupportedResolutionForPreset(range, support);
334
- }
335
-
336
309
  export function getTimeRangeForDateWindow(
337
310
  window: { start: Date | null; end: Date | null } | null,
338
311
  ): TimeRange {
@@ -1,15 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  bucketOhlcSeries,
4
- getVisibleWindow,
5
4
  projectChartData,
6
5
  resolveRenderMode,
7
6
  resolveStableOhlcProjectionOptions,
8
7
  } from "./chart-data";
9
8
  import { stepCursorTowards } from "./cursor-motion";
10
- import { buildChartScene, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
9
+ import { buildChartScene, buildCursorTimeAxisSegments, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
11
10
  import type { PricePoint } from "../../types/financials";
12
- import type { ChartRenderMode, ChartViewState } from "./chart-types";
11
+ import type { ChartRenderMode } from "./chart-types";
13
12
 
14
13
  const aggregationFixture: PricePoint[] = [
15
14
  { date: new Date("2024-01-02"), close: 10, volume: 100 },
@@ -225,48 +224,6 @@ describe("resolveRenderMode", () => {
225
224
  });
226
225
  });
227
226
 
228
- describe("getVisibleWindow", () => {
229
- test("shows the full selected range at default zoom and lets the renderer downsample it", () => {
230
- const history = buildDenseHistory(252);
231
- const viewState: ChartViewState = {
232
- presetRange: "1Y",
233
- bufferRange: "1Y",
234
- activePreset: "1Y",
235
- resolution: "auto",
236
- panOffset: 0,
237
- zoomLevel: 1,
238
- cursorX: null,
239
- cursorY: null,
240
- };
241
-
242
- const window = getVisibleWindow(history, viewState, 80);
243
-
244
- expect(window.points).toHaveLength(history.length);
245
- expect(window.points[0]?.date.toISOString()).toBe(history[0]!.date.toISOString());
246
- expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
247
- });
248
-
249
- test("zooms into the selected range instead of pinning the default view to chart width", () => {
250
- const history = buildDenseHistory(252);
251
- const viewState: ChartViewState = {
252
- presetRange: "1Y",
253
- bufferRange: "1Y",
254
- activePreset: "1Y",
255
- resolution: "auto",
256
- panOffset: 0,
257
- zoomLevel: 2,
258
- cursorX: null,
259
- cursorY: null,
260
- };
261
-
262
- const window = getVisibleWindow(history, viewState, 80);
263
-
264
- expect(window.points).toHaveLength(126);
265
- expect(window.points[0]?.date.toISOString()).toBe(history[126]!.date.toISOString());
266
- expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
267
- });
268
- });
269
-
270
227
  describe("renderChart", () => {
271
228
  test("sizes the y-axis to the visible label width instead of a fixed gutter", () => {
272
229
  expect(resolveChartAxisWidth(["$18", "$17", "$16", "$14"], 4, 10)).toBe(4);
@@ -457,6 +414,21 @@ describe("renderChart", () => {
457
414
  );
458
415
  });
459
416
 
417
+ test("overlays the cursor date on the x-axis without changing its width", () => {
418
+ const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
419
+ const width = 72;
420
+ const segments = buildCursorTimeAxisSegments({
421
+ timeLabels: buildTimeAxis(dates, width),
422
+ width,
423
+ cursorColumn: 36,
424
+ cursorDate: dates[6]!,
425
+ dates,
426
+ });
427
+
428
+ expect(segments.map((segment) => segment.text).join("")).toHaveLength(width);
429
+ expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
430
+ });
431
+
460
432
  test("shows second precision when the visible window reaches second-level data", () => {
461
433
  const dates = Array.from({ length: 6 }, (_, index) => new Date(2026, 0, 5, 9, 30, index * 10));
462
434
 
@@ -25,10 +25,9 @@ describe("projectComparisonChartData", () => {
25
25
  const window = getVisibleComparisonWindow([
26
26
  makeSeries("AAPL", "USD", closes),
27
27
  ], {
28
- timeRange: "ALL",
29
28
  panOffset: 0,
30
29
  zoomLevel: 1,
31
- }, 40);
30
+ });
32
31
 
33
32
  expect(window.dates).toHaveLength(120);
34
33
  expect(window.dates[0]?.toISOString()).toBe("2024-01-01T00:00:00.000Z");
@@ -48,7 +47,6 @@ describe("projectComparisonChartData", () => {
48
47
  ["2024-01-05", 22],
49
48
  ]),
50
49
  ], 8, {
51
- timeRange: "ALL",
52
50
  panOffset: 0,
53
51
  zoomLevel: 1,
54
52
  renderMode: "line",
@@ -72,7 +70,6 @@ describe("projectComparisonChartData", () => {
72
70
  ["2024-01-04", 60],
73
71
  ]),
74
72
  ], 8, {
75
- timeRange: "ALL",
76
73
  panOffset: 0,
77
74
  zoomLevel: 1,
78
75
  renderMode: "line",
@@ -94,7 +91,6 @@ describe("projectComparisonChartData", () => {
94
91
  ["2024-01-03", 2100],
95
92
  ]),
96
93
  ], 8, {
97
- timeRange: "ALL",
98
94
  panOffset: 0,
99
95
  zoomLevel: 1,
100
96
  renderMode: "line",
@@ -83,9 +83,7 @@ function getUniqueSortedDates(series: ComparisonChartSeries[]): Date[] {
83
83
 
84
84
  export function getMaxComparisonPanOffset(
85
85
  series: ComparisonChartSeries[],
86
- _timeRange: TimeRange,
87
86
  zoomLevel: number,
88
- chartWidth: number,
89
87
  ): number {
90
88
  const dates = getUniqueSortedDates(series);
91
89
  const visibleCount = getVisiblePointCount(dates.length, zoomLevel);
@@ -95,7 +93,6 @@ export function getMaxComparisonPanOffset(
95
93
  export function getVisibleComparisonWindow(
96
94
  series: ComparisonChartSeries[],
97
95
  viewState: Pick<ComparisonChartViewState, "panOffset" | "zoomLevel">,
98
- chartWidth: number,
99
96
  ): ComparisonVisibleWindow {
100
97
  const dates = getUniqueSortedDates(series);
101
98
 
@@ -214,7 +211,7 @@ export function projectComparisonChartData(
214
211
  requestedAxisMode: ChartAxisMode,
215
212
  ): ComparisonChartProjection {
216
213
  const normalizedSeries = series.map((entry) => ({ ...entry, points: normalizeSeriesPoints(entry.points) }));
217
- const window = getVisibleComparisonWindow(normalizedSeries, viewState, chartWidth);
214
+ const window = getVisibleComparisonWindow(normalizedSeries, viewState);
218
215
  const axisResolution = resolveComparisonAxisMode(requestedAxisMode, normalizedSeries, window.dates);
219
216
  const requestedMode = normalizeComparisonMode(viewState.renderMode);
220
217
  if (window.dates.length === 0) {