gloomberb 0.6.1 → 0.7.1

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 (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. 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.1",
3
+ "version": "0.7.1",
4
4
  "description": "Bloomberg-style stock portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -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
@@ -1,6 +1,6 @@
1
1
  import { Box, ContextMenuProvider, useNativeRenderer, useRendererHost } from "./ui";
2
2
  import { ToastViewport, useToastHost } from "./ui/toast";
3
- import { useState, useEffect, useCallback, useRef, useMemo } from "react";
3
+ import { useState, useEffect, useCallback, useRef, useMemo, type ReactNode } from "react";
4
4
  import { useShortcut } from "./react/input";
5
5
  import {
6
6
  AppProvider,
@@ -17,12 +17,13 @@ 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";
23
24
  import { PluginRegistry } from "./plugins/registry";
24
25
  import type { TickerRepository } from "./data/ticker-repository";
25
- import { colors, syncTheme } from "./theme/colors";
26
+ import { ThemeProvider, useThemeColors } from "./theme/theme-context";
26
27
  import {
27
28
  createPaneInstance,
28
29
  findPaneInstance,
@@ -44,7 +45,7 @@ import type { TickerRecord } from "./types/ticker";
44
45
  import type { DataProvider } from "./types/data-provider";
45
46
  import type { TickerFinancials } from "./types/financials";
46
47
  import type { BrokerAccount } from "./types/trading";
47
- import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopWindowBridge } from "./types/desktop-window";
48
+ import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState, DesktopWindowBridge } from "./types/desktop-window";
48
49
  import type { DesktopApplicationMenuBridge } from "./types/desktop-menu";
49
50
  import { resolveTickerSearch, upsertTickerFromSearchResult } from "./utils/ticker-search";
50
51
 
@@ -94,6 +95,10 @@ import {
94
95
  createPaneTemplateOrThrow,
95
96
  } from "./components/command-bar/workflow-ops";
96
97
  import { getPaneTemplateDisplayLabel } from "./components/command-bar/pane-template-display";
98
+ import {
99
+ resolveTickerNavigationDetailPane,
100
+ shouldFocusTickerNavigationTarget,
101
+ } from "./plugins/ticker-navigation";
97
102
  import { debugLog } from "./utils/debug-log";
98
103
  import type { MarketDataCoordinator } from "./market-data/coordinator";
99
104
  import { instrumentFromTicker } from "./market-data/request-types";
@@ -135,6 +140,15 @@ interface AppInnerProps {
135
140
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
136
141
  }
137
142
 
143
+ function ThemedAppRoot({ children }: { children: ReactNode }) {
144
+ const themeColors = useThemeColors();
145
+ return (
146
+ <Box flexDirection="column" flexGrow={1} backgroundColor={themeColors.bg}>
147
+ {children}
148
+ </Box>
149
+ );
150
+ }
151
+
138
152
  function AppInner({
139
153
  pluginRegistry,
140
154
  tickerRepository,
@@ -510,6 +524,96 @@ function AppInner({
510
524
  });
511
525
  }, [performRefreshQuote]);
512
526
 
527
+ const refreshTickersBatch = useCallback((entries: Array<{ ticker: TickerRecord; priority: number }>) => {
528
+ const runnable = entries.filter(({ ticker }) => {
529
+ const symbol = ticker.metadata.ticker;
530
+ return !refreshInFlight.has(symbol) && !pendingRefreshesRef.current.financials.has(symbol);
531
+ });
532
+ if (runnable.length === 0) return;
533
+ for (const { ticker } of runnable) {
534
+ pendingRefreshesRef.current.financials.add(ticker.metadata.ticker);
535
+ }
536
+ const priority = Math.min(...runnable.map((entry) => entry.priority));
537
+ refreshQueueRef.current.queue.enqueue({
538
+ key: `financials-batch:${priority}:${runnable.map(({ ticker }) => ticker.metadata.ticker).join(",")}`,
539
+ priority,
540
+ run: async () => {
541
+ const instrumentEntries = runnable.flatMap(({ ticker }) => {
542
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
543
+ return instrument ? [{ ticker, instrument }] : [];
544
+ });
545
+ for (const { ticker } of runnable) {
546
+ const symbol = ticker.metadata.ticker;
547
+ refreshInFlight.add(symbol);
548
+ dispatch({ type: "SET_REFRESHING", symbol, refreshing: true });
549
+ }
550
+ try {
551
+ const entries = await marketData.loadSnapshotsBatch(instrumentEntries.map((entry) => entry.instrument));
552
+ entries.forEach((entry, index) => {
553
+ const ticker = instrumentEntries[index]?.ticker;
554
+ const data = entry.data ?? entry.lastGoodData;
555
+ if (ticker && data) {
556
+ pluginRegistry.events.emit("ticker:refreshed", { symbol: ticker.metadata.ticker, financials: data });
557
+ const currency = data.quote?.currency;
558
+ if (currency) void marketData.loadFxRate(currency).catch(() => {});
559
+ }
560
+ });
561
+ void marketData.loadFxRate(state.config.baseCurrency).catch(() => {});
562
+ } finally {
563
+ for (const { ticker } of runnable) {
564
+ const symbol = ticker.metadata.ticker;
565
+ refreshInFlight.delete(symbol);
566
+ pendingRefreshesRef.current.financials.delete(symbol);
567
+ dispatch({ type: "SET_REFRESHING", symbol, refreshing: false });
568
+ }
569
+ }
570
+ },
571
+ });
572
+ }, [dispatch, marketData, pluginRegistry.events, state.config.baseCurrency]);
573
+
574
+ const refreshQuotesBatch = useCallback((entries: Array<{ ticker: TickerRecord; priority: number }>) => {
575
+ const runnable = entries.filter(({ ticker }) => {
576
+ const symbol = ticker.metadata.ticker;
577
+ return !refreshInFlight.has(symbol)
578
+ && !quoteRefreshInFlight.has(symbol)
579
+ && !pendingRefreshesRef.current.financials.has(symbol)
580
+ && !pendingRefreshesRef.current.quotes.has(symbol);
581
+ });
582
+ if (runnable.length === 0) return;
583
+ for (const { ticker } of runnable) {
584
+ pendingRefreshesRef.current.quotes.add(ticker.metadata.ticker);
585
+ }
586
+ const priority = Math.min(...runnable.map((entry) => entry.priority));
587
+ refreshQueueRef.current.queue.enqueue({
588
+ key: `quotes-batch:${priority}:${runnable.map(({ ticker }) => ticker.metadata.ticker).join(",")}`,
589
+ priority,
590
+ run: async () => {
591
+ const instrumentEntries = runnable.flatMap(({ ticker }) => {
592
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
593
+ return instrument ? [{ ticker, instrument }] : [];
594
+ });
595
+ for (const { ticker } of runnable) {
596
+ quoteRefreshInFlight.add(ticker.metadata.ticker);
597
+ }
598
+ try {
599
+ const entries = await marketData.loadQuotesBatch(instrumentEntries.map((entry) => entry.instrument));
600
+ entries.forEach((entry) => {
601
+ const quote = entry.data ?? entry.lastGoodData;
602
+ const currency = quote?.currency;
603
+ if (currency) void marketData.loadFxRate(currency).catch(() => {});
604
+ });
605
+ void marketData.loadFxRate(state.config.baseCurrency).catch(() => {});
606
+ } finally {
607
+ for (const { ticker } of runnable) {
608
+ const symbol = ticker.metadata.ticker;
609
+ quoteRefreshInFlight.delete(symbol);
610
+ pendingRefreshesRef.current.quotes.delete(symbol);
611
+ }
612
+ }
613
+ },
614
+ });
615
+ }, [marketData, state.config.baseCurrency]);
616
+
513
617
  const primeCachedFinancials = useCallback((entries: Array<{ ticker: TickerRecord; financials: TickerFinancials }>) => {
514
618
  const primeEntries = entries.flatMap(({ ticker, financials }) => {
515
619
  const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
@@ -720,6 +824,8 @@ function AppInner({
720
824
  primeCachedFinancials,
721
825
  refreshTicker,
722
826
  refreshQuote,
827
+ refreshTickersBatch,
828
+ refreshQuotesBatch,
723
829
  autoImportBrokerPositions,
724
830
  persistedBrokerAccounts,
725
831
  }), {
@@ -731,7 +837,7 @@ function AppInner({
731
837
  // Will show empty state
732
838
  }
733
839
  })();
734
- }, [autoImportBrokerPositions, dataProvider, dispatch, primeCachedFinancials, tickerRepository, refreshQuote, refreshTicker, sessionSnapshot, state.config, state.initialized]);
840
+ }, [autoImportBrokerPositions, dataProvider, dispatch, primeCachedFinancials, tickerRepository, refreshQuote, refreshQuotesBatch, refreshTicker, refreshTickersBatch, sessionSnapshot, state.config, state.initialized]);
735
841
 
736
842
  const focusedTickerSymbol = getFocusedTickerSymbol(state);
737
843
 
@@ -1272,8 +1378,9 @@ function AppInner({
1272
1378
  activatePane(instance.instanceId, nextLayout);
1273
1379
  };
1274
1380
 
1275
- pluginRegistry.navigateTickerFn = (rawSymbol) => {
1381
+ pluginRegistry.navigateTickerFn = (rawSymbol, options) => {
1276
1382
  if (isDetachedWindow) return;
1383
+ const sourcePaneId = options?.sourcePaneId ?? stateRef.current.focusedPaneId;
1277
1384
  (async () => {
1278
1385
  try {
1279
1386
  // Resolve or create the ticker in the local database
@@ -1296,45 +1403,31 @@ function AppInner({
1296
1403
  }
1297
1404
  }
1298
1405
 
1299
- // Active panel resolution — navigate the focused or linked detail pane:
1300
- // 1. If the focused pane IS a ticker-detail, retarget it directly
1301
- // 2. If a ticker-detail follows the focused pane, retarget that
1406
+ // Active panel resolution — navigate the initiating or linked detail pane:
1407
+ // 1. If the source pane IS a ticker-detail, retarget it directly
1408
+ // 2. If a ticker-detail follows the source pane, retarget that
1302
1409
  // 3. Any follow-mode ticker-detail in the layout
1303
1410
  // 4. Any ticker-detail in the layout
1304
1411
  // 5. Fall back to pinning a new pane
1305
1412
  const currentState = stateRef.current;
1306
1413
  const currentLayout = currentState.config.layout;
1307
- const focused = currentState.focusedPaneId;
1308
-
1309
- const focusedInstance = focused
1310
- ? findPaneInstance(currentLayout, focused)
1311
- : null;
1312
-
1313
- const detailPane =
1314
- (focusedInstance?.paneId === "ticker-detail" && isPaneInLayout(currentLayout, focusedInstance.instanceId)
1315
- ? focusedInstance
1316
- : null)
1317
- ?? currentLayout.instances.find((inst) =>
1318
- inst.paneId === "ticker-detail"
1319
- && inst.binding?.kind === "follow"
1320
- && inst.binding.sourceInstanceId === focused
1321
- && isPaneInLayout(currentLayout, inst.instanceId),
1322
- )
1323
- ?? currentLayout.instances.find((inst) =>
1324
- inst.paneId === "ticker-detail"
1325
- && inst.binding?.kind === "follow"
1326
- && isPaneInLayout(currentLayout, inst.instanceId),
1327
- )
1328
- ?? currentLayout.instances.find((inst) =>
1329
- inst.paneId === "ticker-detail"
1330
- && isPaneInLayout(currentLayout, inst.instanceId),
1331
- );
1414
+ const detailPane = resolveTickerNavigationDetailPane(currentLayout, sourcePaneId);
1415
+ const focusIfStillOwned = (paneId: string, layout: LayoutConfig) => {
1416
+ if (!shouldFocusTickerNavigationTarget({
1417
+ sourcePaneId,
1418
+ currentFocusedPaneId: stateRef.current.focusedPaneId,
1419
+ targetPaneId: paneId,
1420
+ })) {
1421
+ return;
1422
+ }
1423
+ activatePane(paneId, layout);
1424
+ };
1332
1425
 
1333
1426
  if (detailPane) {
1334
1427
  if (detailPane.binding?.kind === "follow") {
1335
1428
  const sourceId = detailPane.binding.sourceInstanceId;
1336
1429
  dispatch({ type: "UPDATE_PANE_STATE", paneId: sourceId, patch: { cursorSymbol: symbol } });
1337
- activatePane(detailPane.instanceId, currentLayout);
1430
+ focusIfStillOwned(detailPane.instanceId, currentLayout);
1338
1431
  } else {
1339
1432
  const nextLayout = {
1340
1433
  ...currentLayout,
@@ -1345,9 +1438,13 @@ function AppInner({
1345
1438
  )),
1346
1439
  };
1347
1440
  persistLayout(nextLayout);
1348
- activatePane(detailPane.instanceId, nextLayout);
1441
+ focusIfStillOwned(detailPane.instanceId, nextLayout);
1349
1442
  }
1350
- } else {
1443
+ } else if (shouldFocusTickerNavigationTarget({
1444
+ sourcePaneId,
1445
+ currentFocusedPaneId: stateRef.current.focusedPaneId,
1446
+ targetPaneId: null,
1447
+ })) {
1351
1448
  pluginRegistry.pinTicker(symbol, { floating: false });
1352
1449
  }
1353
1450
  } catch (err) {
@@ -1441,18 +1538,20 @@ function AppInner({
1441
1538
  if (state.commandBarOpen || state.inputCaptured) return;
1442
1539
 
1443
1540
  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
- ];
1541
+ const paneOrder = getVisiblePaneCycleOrder(
1542
+ state.config.layout,
1543
+ pluginRegistry,
1544
+ state.config.disabledPlugins,
1545
+ );
1546
+ if (paneOrder.length === 0) return;
1450
1547
 
1451
1548
  if (event.shift) {
1452
1549
  dispatch({ type: "FOCUS_PREV", paneOrder });
1453
1550
  } else {
1454
1551
  dispatch({ type: "FOCUS_NEXT", paneOrder });
1455
1552
  }
1553
+ event.preventDefault();
1554
+ event.stopPropagation();
1456
1555
  } else if (!isDetachedWindow && event.name === "q" && !(focusedPane?.paneId === "ticker-detail" && focusedDetailTab === "financials")) {
1457
1556
  rendererHost.requestExit();
1458
1557
  } else if (event.name === "r") {
@@ -1494,20 +1593,20 @@ function AppInner({
1494
1593
  if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
1495
1594
  return (
1496
1595
  <ContextMenuProvider pluginRegistry={pluginRegistry}>
1497
- <Box flexDirection="column" flexGrow={1} backgroundColor={colors.bg}>
1596
+ <ThemedAppRoot>
1498
1597
  <DetachedPaneShell
1499
1598
  pluginRegistry={pluginRegistry}
1500
1599
  desktopWindowBridge={{ ...desktopWindowBridge, kind: "detached", paneId: desktopWindowBridge.paneId }}
1501
1600
  />
1502
1601
  <ToastViewport position="bottom-right" />
1503
- </Box>
1602
+ </ThemedAppRoot>
1504
1603
  </ContextMenuProvider>
1505
1604
  );
1506
1605
  }
1507
1606
 
1508
1607
  return (
1509
1608
  <ContextMenuProvider pluginRegistry={pluginRegistry}>
1510
- <Box flexDirection="column" flexGrow={1} backgroundColor={colors.bg}>
1609
+ <ThemedAppRoot>
1511
1610
  <Header />
1512
1611
  <Shell
1513
1612
  pluginRegistry={pluginRegistry}
@@ -1525,7 +1624,7 @@ function AppInner({
1525
1624
  />
1526
1625
  )}
1527
1626
  <ToastViewport position="bottom-right" />
1528
- </Box>
1627
+ </ThemedAppRoot>
1529
1628
  </ContextMenuProvider>
1530
1629
  );
1531
1630
  }
@@ -1537,6 +1636,7 @@ interface AppProps {
1537
1636
  desktopWindowBridge?: DesktopWindowBridge;
1538
1637
  desktopApplicationMenuBridge?: DesktopApplicationMenuBridge;
1539
1638
  desktopSnapshot?: DesktopSharedStateSnapshot | null;
1639
+ desktopThemePreview?: DesktopThemePreviewState | null;
1540
1640
  }
1541
1641
 
1542
1642
  export function App({
@@ -1546,6 +1646,7 @@ export function App({
1546
1646
  desktopWindowBridge,
1547
1647
  desktopApplicationMenuBridge,
1548
1648
  desktopSnapshot = null,
1649
+ desktopThemePreview = null,
1549
1650
  }: AppProps) {
1550
1651
  const renderer = useNativeRenderer();
1551
1652
  const effectiveInitialConfig = useMemo(() => {
@@ -1576,9 +1677,6 @@ export function App({
1576
1677
  });
1577
1678
  const [showOnboarding, setShowOnboarding] = useState(!effectiveInitialConfig.onboardingComplete);
1578
1679
 
1579
- // Keep the shared palette aligned before this render builds any JSX that reads `colors`.
1580
- syncTheme(config.theme);
1581
-
1582
1680
  useEffect(() => bindAppActivity(renderer), [renderer]);
1583
1681
 
1584
1682
  const services = useMemo(() => {
@@ -1634,14 +1732,16 @@ export function App({
1634
1732
 
1635
1733
  if (showOnboarding) {
1636
1734
  return (
1637
- <OnboardingWizard
1638
- config={config}
1639
- pluginRegistry={services.pluginRegistry}
1640
- onComplete={(updatedConfig) => {
1641
- setConfig(updatedConfig);
1642
- setShowOnboarding(false);
1643
- }}
1644
- />
1735
+ <ThemeProvider themeId={config.theme}>
1736
+ <OnboardingWizard
1737
+ config={config}
1738
+ pluginRegistry={services.pluginRegistry}
1739
+ onComplete={(updatedConfig) => {
1740
+ setConfig(updatedConfig);
1741
+ setShowOnboarding(false);
1742
+ }}
1743
+ />
1744
+ </ThemeProvider>
1645
1745
  );
1646
1746
  }
1647
1747
 
@@ -1652,6 +1752,7 @@ export function App({
1652
1752
  sessionSnapshot={sessionSnapshot}
1653
1753
  desktopBridge={desktopWindowBridge}
1654
1754
  desktopSnapshot={desktopSnapshot}
1755
+ initialThemePreview={desktopThemePreview}
1655
1756
  >
1656
1757
  <AppInner
1657
1758
  pluginRegistry={services.pluginRegistry}
@@ -26,6 +26,8 @@ export function assetDataProvider(provider: AssetDataProvider): AssetDataCapabil
26
26
  operations: {
27
27
  canProvide: op((input: any) => provider.canProvide?.(input.ticker, input.exchange, input.context) ?? true, "query"),
28
28
  getCachedFinancialsForTargets: op((input: any) => provider.getCachedFinancialsForTargets?.(input.targets ?? [], input.options) ?? new Map()),
29
+ getQuotesBatch: op((input: any) => provider.getQuotesBatch?.(input.targets ?? [], input.options) ?? Promise.resolve([])),
30
+ getTickerFinancialsBatch: op((input: any) => provider.getTickerFinancialsBatch?.(input.targets ?? [], input.options) ?? Promise.resolve([])),
29
31
  getTickerFinancials: op((input: any) => provider.getTickerFinancials(input.ticker, input.exchange, input.context)),
30
32
  getQuote: op((input: any) => provider.getQuote(input.ticker, input.exchange, input.context)),
31
33
  getExchangeRate: op((input: any) => provider.getExchangeRate(input.fromCurrency)),
@@ -95,6 +97,7 @@ export function newsProvider(options: {
95
97
  supports: op((input: any) => options.provider.supports?.(input.query) ?? true, "query"),
96
98
  getCachedNews: op((input: any) => options.provider.getCachedNews?.(input.query) ?? [], "query"),
97
99
  fetchNews: op((input: any) => options.provider.fetchNews(input.query), "query"),
100
+ fetchNewsStory: op((input: any) => options.provider.fetchNewsStory?.(input.storyId) ?? null, "query"),
98
101
  },
99
102
  };
100
103
  }
@@ -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