gloomberb 0.9.2 → 0.9.4

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 (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -2,12 +2,14 @@
2
2
  /** @jsxImportSource react */
3
3
  import {
4
4
  forwardRef,
5
+ useCallback,
5
6
  useEffect,
6
7
  useImperativeHandle,
7
8
  useRef,
8
9
  useState,
9
10
  type CSSProperties,
10
11
  type KeyboardEvent,
12
+ type RefObject,
11
13
  } from "react";
12
14
  import type { InputRenderable, TextareaRenderable } from "../../../../ui/host";
13
15
  import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../input-host";
@@ -79,6 +81,65 @@ function useEditableValue(props: Record<string, unknown>) {
79
81
  return { value, valueRef, setValue };
80
82
  }
81
83
 
84
+ function useLatestRef<T>(value: T): RefObject<T> {
85
+ const ref = useRef(value);
86
+ ref.current = value;
87
+ return ref;
88
+ }
89
+
90
+ function useSyncedEditableElement<T extends HTMLInputElement | HTMLTextAreaElement>({
91
+ elementRef,
92
+ propsRef,
93
+ valueRef,
94
+ handleValueChange,
95
+ active,
96
+ }: {
97
+ elementRef: RefObject<T | null>;
98
+ propsRef: RefObject<Record<string, unknown>>;
99
+ valueRef: RefObject<string>;
100
+ handleValueChange: (nextValue: string) => void;
101
+ active: boolean;
102
+ }): () => string {
103
+ const syncElementValue = useCallback(() => {
104
+ const nextValue = elementRef.current?.value;
105
+ if (typeof nextValue !== "string") return valueRef.current;
106
+ if (nextValue !== valueRef.current) {
107
+ handleValueChange(nextValue);
108
+ }
109
+ return nextValue;
110
+ }, [elementRef, handleValueChange, valueRef]);
111
+
112
+ useEffect(() => {
113
+ if (!active) return;
114
+ let animationFrame: number | null = null;
115
+ const syncDomValue = () => {
116
+ syncElementValue();
117
+ animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
118
+ };
119
+ animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
120
+ return () => {
121
+ if (animationFrame !== null) globalThis.cancelAnimationFrame?.(animationFrame);
122
+ };
123
+ }, [active, syncElementValue]);
124
+
125
+ useEffect(() => {
126
+ if (!active) return;
127
+ const commitBeforeOutsideMouseDown = (event: globalThis.MouseEvent) => {
128
+ const element = elementRef.current;
129
+ const target = event.target;
130
+ if (!element || (target instanceof Node && element.contains(target))) return;
131
+ const nextValue = syncElementValue();
132
+ callTextHandler(propsRef.current.onBlur, nextValue);
133
+ };
134
+ document.addEventListener("mousedown", commitBeforeOutsideMouseDown, true);
135
+ return () => {
136
+ document.removeEventListener("mousedown", commitBeforeOutsideMouseDown, true);
137
+ };
138
+ }, [active, elementRef, propsRef, syncElementValue]);
139
+
140
+ return syncElementValue;
141
+ }
142
+
82
143
  type WebVisualCursor = TextareaRenderable["visualCursor"];
83
144
 
84
145
  function textareaColumnCount(props: Record<string, unknown>, element: HTMLTextAreaElement | null): number {
@@ -149,8 +210,10 @@ function textareaMetrics(
149
210
 
150
211
  export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(function WebInput(props, ref) {
151
212
  const elementRef = useRef<HTMLInputElement | null>(null);
213
+ const propsRef = useLatestRef(props);
152
214
  const { value, valueRef, setValue } = useEditableValue(props);
153
215
  const [cursorOffset, setCursorOffset] = useState(value.length);
216
+ const [domFocused, setDomFocused] = useState(false);
154
217
  const applyCursorOffset = (offset: number) => {
155
218
  setCursorOffset(offset);
156
219
  queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
@@ -165,7 +228,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
165
228
 
166
229
  useImperativeHandle(ref, () => ({
167
230
  editBuffer: {
168
- getText: () => valueRef.current,
231
+ getText: () => elementRef.current?.value ?? valueRef.current,
169
232
  setText: (nextText: string) => setValue(nextText),
170
233
  },
171
234
  get cursorOffset() {
@@ -175,18 +238,25 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
175
238
  focus: () => elementRef.current?.focus(),
176
239
  }), [applyCursorOffset, cursorOffset, setValue, valueRef]);
177
240
 
178
- const handleValueChange = (nextValue: string) => {
241
+ const handleValueChange = useCallback((nextValue: string) => {
179
242
  setValue(nextValue);
180
243
  setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
181
- callTextHandler(props.onInput, nextValue);
182
- callTextHandler(props.onChange, nextValue);
183
- callTextHandler(props.onCursorChange, nextValue);
184
- };
244
+ callTextHandler(propsRef.current.onInput, nextValue);
245
+ callTextHandler(propsRef.current.onChange, nextValue);
246
+ callTextHandler(propsRef.current.onCursorChange, nextValue);
247
+ }, [propsRef, setValue]);
248
+ const syncElementValue = useSyncedEditableElement({
249
+ elementRef,
250
+ propsRef,
251
+ valueRef,
252
+ handleValueChange,
253
+ active: props.focused === true || domFocused,
254
+ });
185
255
 
186
256
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
187
- if (event.key === "Enter" && typeof props.onSubmit === "function") {
257
+ if (event.key === "Enter" && typeof propsRef.current.onSubmit === "function") {
188
258
  event.preventDefault();
189
- (props.onSubmit as (value: string) => void)(valueRef.current);
259
+ (propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
190
260
  }
191
261
  };
192
262
 
@@ -200,7 +270,16 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
200
270
  autoComplete="off"
201
271
  spellCheck={false}
202
272
  placeholder={getStringProp(props, "placeholder")}
273
+ onInput={(event) => handleValueChange(event.currentTarget.value)}
203
274
  onChange={(event) => handleValueChange(event.currentTarget.value)}
275
+ onFocus={() => {
276
+ setDomFocused(true);
277
+ callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
278
+ }}
279
+ onBlur={() => {
280
+ setDomFocused(false);
281
+ callTextHandler(propsRef.current.onBlur, syncElementValue());
282
+ }}
204
283
  onKeyDown={handleKeyDown}
205
284
  onContextMenu={(event) => {
206
285
  if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
@@ -211,7 +290,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
211
290
  }}
212
291
  onSelect={() => {
213
292
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
214
- callTextHandler(props.onCursorChange, valueRef.current);
293
+ callTextHandler(propsRef.current.onCursorChange, valueRef.current);
215
294
  }}
216
295
  style={textInputStyle(props, false)}
217
296
  />
@@ -220,8 +299,10 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
220
299
 
221
300
  export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown>>(function WebTextarea(props, ref) {
222
301
  const elementRef = useRef<HTMLTextAreaElement | null>(null);
302
+ const propsRef = useLatestRef(props);
223
303
  const { value, valueRef, setValue } = useEditableValue(props);
224
304
  const [cursorOffset, setCursorOffset] = useState(value.length);
305
+ const [domFocused, setDomFocused] = useState(false);
225
306
  const applyCursorOffset = (offset: number) => {
226
307
  setCursorOffset(offset);
227
308
  queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
@@ -236,7 +317,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
236
317
 
237
318
  useImperativeHandle(ref, () => ({
238
319
  editBuffer: {
239
- getText: () => valueRef.current,
320
+ getText: () => elementRef.current?.value ?? valueRef.current,
240
321
  setText: (nextText: string) => setValue(nextText),
241
322
  },
242
323
  get cursorOffset() {
@@ -271,18 +352,25 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
271
352
  clearLineHighlights: () => {},
272
353
  }), [applyCursorOffset, cursorOffset, props, setValue, valueRef]);
273
354
 
274
- const handleValueChange = (nextValue: string) => {
355
+ const handleValueChange = useCallback((nextValue: string) => {
275
356
  setValue(nextValue);
276
357
  setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
277
- callTextHandler(props.onInput, nextValue);
278
- callTextHandler(props.onChange, nextValue);
279
- callTextHandler(props.onCursorChange, nextValue);
280
- };
358
+ callTextHandler(propsRef.current.onInput, nextValue);
359
+ callTextHandler(propsRef.current.onChange, nextValue);
360
+ callTextHandler(propsRef.current.onCursorChange, nextValue);
361
+ }, [propsRef, setValue]);
362
+ const syncElementValue = useSyncedEditableElement({
363
+ elementRef,
364
+ propsRef,
365
+ valueRef,
366
+ handleValueChange,
367
+ active: props.focused === true || domFocused,
368
+ });
281
369
 
282
370
  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
283
- if (event.key === "Enter" && !event.shiftKey && typeof props.onSubmit === "function") {
371
+ if (event.key === "Enter" && !event.shiftKey && typeof propsRef.current.onSubmit === "function") {
284
372
  event.preventDefault();
285
- (props.onSubmit as (value: string) => void)(valueRef.current);
373
+ (propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
286
374
  }
287
375
  };
288
376
 
@@ -296,7 +384,16 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
296
384
  autoComplete="off"
297
385
  spellCheck={false}
298
386
  placeholder={getStringProp(props, "placeholder")}
387
+ onInput={(event) => handleValueChange(event.currentTarget.value)}
299
388
  onChange={(event) => handleValueChange(event.currentTarget.value)}
389
+ onFocus={() => {
390
+ setDomFocused(true);
391
+ callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
392
+ }}
393
+ onBlur={() => {
394
+ setDomFocused(false);
395
+ callTextHandler(propsRef.current.onBlur, syncElementValue());
396
+ }}
300
397
  onKeyDown={handleKeyDown}
301
398
  onContextMenu={(event) => {
302
399
  if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
@@ -307,7 +404,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
307
404
  }}
308
405
  onSelect={() => {
309
406
  setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
310
- callTextHandler(props.onCursorChange, valueRef.current);
407
+ callTextHandler(propsRef.current.onCursorChange, valueRef.current);
311
408
  }}
312
409
  style={textInputStyle(props, true)}
313
410
  />
@@ -21,6 +21,7 @@ export function WebTabs({
21
21
  closeMode = "always",
22
22
  addLabel = "+",
23
23
  onAdd,
24
+ focused = false,
24
25
  palette,
25
26
  }: HostTabsProps) {
26
27
  const activeTabRef = useRef<HTMLButtonElement | null>(null);
@@ -112,16 +113,20 @@ export function WebTabs({
112
113
  paddingBlock: 0,
113
114
  paddingBottom: showUnderline ? 2 : 0,
114
115
  margin: 0,
115
- border: 0,
116
+ border: "1px solid transparent",
116
117
  borderRadius: variant === "underline" ? 5 : 6,
117
118
  background: resolveTabBackground(active, hovered),
119
+ boxSizing: "border-box",
118
120
  font: "inherit",
119
121
  fontSize: tabFontSize,
120
122
  fontWeight: active ? 700 : 500,
121
123
  lineHeight: 1,
122
124
  textAlign: "center",
123
125
  whiteSpace: "nowrap",
124
- transition: "background-color 110ms ease, color 110ms ease, box-shadow 110ms ease",
126
+ borderColor: active && focused && variant !== "pill"
127
+ ? `color-mix(in srgb, ${palette.activeUnderline} 35%, transparent)`
128
+ : "transparent",
129
+ transition: "background-color 110ms ease, border-color 110ms ease, color 110ms ease",
125
130
  cursor: disabled ? "default" : "pointer",
126
131
  } satisfies CssVars;
127
132
 
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { renderAsciiText } from "../../../../ui/ascii-font";
3
+ import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
4
+
5
+ describe("desktop web ASCII text", () => {
6
+ test("keeps the legacy Gloomberb wordmark on macOS", () => {
7
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "darwin")).toBe("legacy");
8
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "MacIntel")).toBe("legacy");
9
+ expect(webAsciiTextLines("Gloomberb", "wordmark", "darwin")).toEqual(
10
+ renderAsciiText("Gloomberb", "wordmark"),
11
+ );
12
+ });
13
+
14
+ test("uses the web-safe Gloomberb wordmark off macOS", () => {
15
+ const lines = webAsciiTextLines("Gloomberb", "wordmark", "win32");
16
+
17
+ expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "win32")).toBe("compat");
18
+ expect(lines).toHaveLength(5);
19
+ expect(lines[0]).toContain("____");
20
+ expect(lines).not.toEqual(renderAsciiText("Gloomberb", "wordmark"));
21
+ });
22
+ });
@@ -7,21 +7,17 @@ import {
7
7
  type StyledTextChunk,
8
8
  type TextProps,
9
9
  } from "../../../../ui/host";
10
- import { renderAsciiText } from "../../../../ui/ascii-font";
11
10
  import { WEB_CELL_WIDTH } from "../input-host";
11
+ import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
12
12
  import { hasDirectMouseHandler, mouseHandlers } from "./mouse";
13
13
  import { cleanDomProps, commonStyle, textStyle } from "./style";
14
14
 
15
- const WEB_GLOOMBERB_WORDMARK = [
16
- " ____ _ _ _ ",
17
- " / ___| | ___ ___ _ __ ___ | |__ ___ _ __| |__ ",
18
- "| | _| |/ _ \\ / _ \\| '_ ` _ \\| '_ \\ / _ \\ '__| '_ \\ ",
19
- "| |_| | | (_) | (_) | | | | | | |_) | __/ | | |_) |",
20
- " \\____|_|\\___/ \\___/|_| |_| |_|_.__/ \\___|_| |_.__/ ",
21
- ];
22
-
23
15
  const WEB_WORDMARK_CHAR_WIDTH_PX = Math.max(10, WEB_CELL_WIDTH);
24
16
 
17
+ interface WebAsciiTextProps extends AsciiTextProps {
18
+ desktopPlatform?: string;
19
+ }
20
+
25
21
  function isStyledTextContent(value: unknown): value is { chunks: StyledTextChunk[] } {
26
22
  return value instanceof StyledText
27
23
  || (!!value && typeof value === "object" && Array.isArray((value as { chunks?: unknown }).chunks));
@@ -100,16 +96,17 @@ export function WebAsciiText({
100
96
  bg,
101
97
  backgroundColor,
102
98
  selectable = false,
99
+ desktopPlatform,
103
100
  ...props
104
- }: AsciiTextProps) {
105
- const isWordmark = font === "wordmark";
106
- const lines = isWordmark && text.trim().toLowerCase() === "gloomberb"
107
- ? WEB_GLOOMBERB_WORDMARK
108
- : renderAsciiText(text, font);
101
+ }: WebAsciiTextProps) {
102
+ const wordmarkVariant = webAsciiTextWordmarkVariant(text, font, desktopPlatform);
103
+ const isCompatWordmark = wordmarkVariant === "compat";
104
+ const isLegacyWordmark = wordmarkVariant === "legacy";
105
+ const lines = webAsciiTextLines(text, font, desktopPlatform);
109
106
  const resolvedColor = color ?? fg;
110
107
  const resolvedBackground = bg ?? backgroundColor;
111
- const lineHeightPx = isWordmark ? 16 : 12;
112
- const wordmarkWidthPx = isWordmark
108
+ const lineHeightPx = isCompatWordmark ? 16 : 12;
109
+ const wordmarkWidthPx = isCompatWordmark
113
110
  ? Math.max(...lines.map((line) => line.length)) * WEB_WORDMARK_CHAR_WIDTH_PX
114
111
  : undefined;
115
112
  return (
@@ -118,15 +115,16 @@ export function WebAsciiText({
118
115
  data-gloom-role={(props["data-gloom-role"] as string | undefined) ?? "ascii-text"}
119
116
  style={{
120
117
  ...commonStyle({ ...props, fg: resolvedColor, bg: resolvedBackground }),
121
- display: "block",
118
+ display: isLegacyWordmark ? "flex" : "block",
119
+ flexDirection: isLegacyWordmark ? "column" : undefined,
122
120
  flexShrink: 0,
123
121
  ...(wordmarkWidthPx != null && props.width == null
124
122
  ? { width: `${wordmarkWidthPx}px`, minWidth: `${wordmarkWidthPx}px` }
125
123
  : {}),
126
124
  color: resolvedColor,
127
125
  backgroundColor: resolvedBackground,
128
- fontFamily: isWordmark ? "\"Cascadia Mono\", Consolas, \"Courier New\", monospace" : undefined,
129
- fontSize: isWordmark ? "16px" : undefined,
126
+ fontFamily: isCompatWordmark ? "\"Cascadia Mono\", Consolas, \"Courier New\", monospace" : undefined,
127
+ fontSize: isCompatWordmark ? "16px" : undefined,
130
128
  fontVariantLigatures: "none",
131
129
  lineHeight: `${lineHeightPx}px`,
132
130
  whiteSpace: "pre",
@@ -135,7 +133,13 @@ export function WebAsciiText({
135
133
  ...(props.style as CSSProperties | undefined),
136
134
  }}
137
135
  >
138
- {lines.join("\n")}
136
+ {isLegacyWordmark
137
+ ? lines.map((line, index) => (
138
+ <span key={index} style={{ display: "block", height: "12px", lineHeight: "12px" }}>
139
+ {line}
140
+ </span>
141
+ ))
142
+ : lines.join("\n")}
139
143
  </div>
140
144
  );
141
145
  }
@@ -170,6 +170,17 @@ body.gloom-dragging {
170
170
  [data-gloom-role="status-bar"] span {
171
171
  line-height: var(--cell-h);
172
172
  }
173
+ [data-gloom-role="pane-body"] {
174
+ overflow: hidden;
175
+ }
176
+ [data-gloom-role="pane-content"] {
177
+ overflow: hidden;
178
+ }
179
+ [data-gloom-role="pane-content"] > * {
180
+ min-width: 0;
181
+ min-height: 0;
182
+ max-height: 100%;
183
+ }
173
184
  [data-gloom-role="pane-footer"] {
174
185
  flex: 0 0 var(--cell-h);
175
186
  height: var(--cell-h);
@@ -137,7 +137,7 @@ export function createWebUiHost(desktopPlatform?: string): UiHost {
137
137
  *
138
138
  </span>
139
139
  ),
140
- AsciiText: WebAsciiText,
140
+ AsciiText: (props) => <WebAsciiText {...props} desktopPlatform={desktopPlatform} />,
141
141
  };
142
142
  }
143
143
 
@@ -138,8 +138,14 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
138
138
  get isDestroyed() {
139
139
  return renderer.isDestroyed;
140
140
  },
141
+ get currentFocusedRenderable() {
142
+ return renderer.currentFocusedRenderable ?? null;
143
+ },
144
+ get currentFocusedEditor() {
145
+ return renderer.currentFocusedEditor ?? null;
146
+ },
141
147
  get keyInput() {
142
- return renderer.keyInput;
148
+ return renderer.keyInput as unknown as NativeRendererHost["keyInput"];
143
149
  },
144
150
  on: (event, handler) => renderer.on(event, handler),
145
151
  off: (event, handler) => renderer.off(event, handler),
@@ -147,7 +153,17 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
147
153
  registerLifecyclePass: (renderable) => renderer.registerLifecyclePass(renderable as any),
148
154
  unregisterLifecyclePass: (renderable) => renderer.unregisterLifecyclePass(renderable as any),
149
155
  getSelection: () => renderer.getSelection(),
156
+ getCursorState: () => renderer.getCursorState(),
157
+ setCursorPosition: (x, y, visible = true) => renderer.setCursorPosition(x, y, visible),
158
+ addPostProcessFn: (processFn) => renderer.addPostProcessFn(processFn as any),
159
+ removePostProcessFn: (processFn) => renderer.removePostProcessFn(processFn as any),
150
160
  copyToClipboardOSC52: (text) => renderer.copyToClipboardOSC52(text),
161
+ captureMouseRenderable(renderable) {
162
+ const capture = (renderer as unknown as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
163
+ if (typeof capture === "function") {
164
+ capture.call(renderer, renderable);
165
+ }
166
+ },
151
167
  write(data) {
152
168
  if (renderer.isDestroyed) return false;
153
169
  const writer = (renderer as unknown as { writeOut?: (payload: string | Uint8Array) => void }).writeOut;
@@ -16,18 +16,26 @@ import { ToastHostProvider } from "../../ui/toast";
16
16
  import { colors } from "../../theme/colors";
17
17
  import { startMainThreadMonitor } from "../../utils/main-thread-monitor";
18
18
  import { measurePerfAsync } from "../../utils/perf-marks";
19
+ import type { CliLaunchRequest } from "../../types/plugin";
19
20
 
20
- export async function startOpenTuiApp(): Promise<void> {
21
+ export interface StartOpenTuiAppOptions {
22
+ externalPlugins?: Awaited<ReturnType<typeof loadExternalPlugins>>;
23
+ cliArgs?: string[];
24
+ skipCliDispatch?: boolean;
25
+ cliLaunchRequest?: CliLaunchRequest | null;
26
+ }
27
+
28
+ export async function startOpenTuiApp(options: StartOpenTuiAppOptions = {}): Promise<void> {
21
29
  setConfigStoreHost(nodeConfigStoreHost);
22
30
  debugLog.interceptConsole();
23
31
 
24
32
  const appLog = debugLog.createLogger("app");
25
33
  appLog.info("Gloomberb starting");
26
34
 
27
- const cliArgs = process.argv.slice(2);
28
- const externalPlugins = await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins());
29
- let cliLaunchRequest = null;
30
- if (cliArgs.length > 0) {
35
+ const cliArgs = options.cliArgs ?? process.argv.slice(2);
36
+ const externalPlugins = options.externalPlugins ?? await measurePerfAsync("startup.opentui.load-external-plugins", () => loadExternalPlugins());
37
+ let cliLaunchRequest = options.cliLaunchRequest ?? null;
38
+ if (!options.skipCliDispatch && cliArgs.length > 0) {
31
39
  const dispatchResult = await dispatchCli(cliArgs, { externalPlugins });
32
40
  if (dispatchResult.kind === "handled") return;
33
41
  if (dispatchResult.kind === "launch-ui") {
@@ -1,11 +1,25 @@
1
1
  import { RGBA, StyledText as OpenTuiStyledText, SyntaxStyle, TextAttributes as OpenTuiTextAttributes } from "@opentui/core";
2
- import { createElement } from "react";
2
+ import { createElement, forwardRef, type ReactNode } from "react";
3
3
  import "opentui-spinner/react";
4
4
  import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
5
5
  import { renderAsciiText } from "../../ui/ascii-font";
6
6
  import { OpenTuiImageSurface } from "./image/surface";
7
7
  import { OpenTuiChartSurface } from "./chart-surface";
8
8
 
9
+ interface OpenTuiPrimitiveProps {
10
+ children?: ReactNode;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ function createOpenTuiPrimitive(tagName: string) {
15
+ return forwardRef<unknown, OpenTuiPrimitiveProps>(function OpenTuiPrimitive(
16
+ { children, ...props },
17
+ ref,
18
+ ) {
19
+ return createElement(tagName as any, { ...props, ref }, children as ReactNode);
20
+ });
21
+ }
22
+
9
23
  function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
10
24
  const flags = typeof appAttributes === "number" ? appAttributes : 0;
11
25
  let attributes = 0;
@@ -37,41 +51,59 @@ function mapTextContent(content: unknown): unknown {
37
51
  fg: mapColor(chunk.fg),
38
52
  bg: mapColor(chunk.bg),
39
53
  attributes: mapTextAttributes(chunk.attributes as number | undefined),
40
- })));
54
+ })) as any);
41
55
  }
42
56
  return content;
43
57
  }
44
58
 
59
+ const OpenTuiBox = createOpenTuiPrimitive("box");
60
+ const OpenTuiScrollBox = createOpenTuiPrimitive("scrollbox");
61
+ const OpenTuiInput = createOpenTuiPrimitive("input");
62
+ const OpenTuiTextarea = createOpenTuiPrimitive("textarea");
63
+ const OpenTuiSpinnerMark = createOpenTuiPrimitive("spinner");
64
+
65
+ const OpenTuiText = forwardRef<unknown, TextProps>(function OpenTuiText({ children, ...props }, ref) {
66
+ const textProps = stripTextProps(props);
67
+ return createElement("text" as any, {
68
+ ...textProps,
69
+ ref,
70
+ content: mapTextContent(textProps.content),
71
+ attributes: mapTextAttributes(props.attributes as number | undefined, props),
72
+ }, children as ReactNode);
73
+ });
74
+
75
+ const OpenTuiSpan = forwardRef<unknown, TextProps>(function OpenTuiSpan({ children, ...props }, ref) {
76
+ return createElement("span" as any, {
77
+ ...stripTextProps(props),
78
+ ref,
79
+ attributes: mapTextAttributes(props.attributes as number | undefined, props),
80
+ }, children as ReactNode);
81
+ });
82
+
83
+ const OpenTuiStrong = forwardRef<unknown, TextProps>(function OpenTuiStrong({ children, ...props }, ref) {
84
+ return createElement("strong" as any, { ...stripTextProps(props), ref }, children as ReactNode);
85
+ });
86
+
87
+ const OpenTuiUnderline = forwardRef<unknown, TextProps>(function OpenTuiUnderline({ children, ...props }, ref) {
88
+ return createElement("u" as any, { ...stripTextProps(props), ref }, children as ReactNode);
89
+ });
90
+
45
91
  export const openTuiUiHost: UiHost = {
46
92
  kind: "opentui",
47
93
  capabilities: {
48
94
  nativeCharts: true,
49
95
  },
50
- Box: ({ children, ...props }) => <box {...props}>{children}</box>,
51
- Text: ({ children, ...props }) => {
52
- const textProps = stripTextProps(props);
53
- return (
54
- <text
55
- {...textProps}
56
- content={mapTextContent(textProps.content)}
57
- attributes={mapTextAttributes(props.attributes as number | undefined, props)}
58
- >
59
- {children}
60
- </text>
61
- );
62
- },
63
- Span: ({ children, ...props }) => createElement("span" as any, {
64
- ...stripTextProps(props),
65
- attributes: mapTextAttributes(props.attributes as number | undefined, props),
66
- }, children),
67
- Strong: ({ children, ...props }) => createElement("strong" as any, stripTextProps(props), children),
68
- Underline: ({ children, ...props }) => createElement("u" as any, stripTextProps(props), children),
69
- ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
70
- Input: ({ children, ...props }) => createElement("input" as any, props, children),
71
- Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
96
+ Box: OpenTuiBox as UiHost["Box"],
97
+ Text: OpenTuiText as UiHost["Text"],
98
+ Span: OpenTuiSpan as UiHost["Span"],
99
+ Strong: OpenTuiStrong as UiHost["Strong"],
100
+ Underline: OpenTuiUnderline as UiHost["Underline"],
101
+ ScrollBox: OpenTuiScrollBox as UiHost["ScrollBox"],
102
+ Input: OpenTuiInput as UiHost["Input"],
103
+ Textarea: OpenTuiTextarea as UiHost["Textarea"],
72
104
  ChartSurface: OpenTuiChartSurface,
73
105
  ImageSurface: OpenTuiImageSurface,
74
- SpinnerMark: ({ children, ...props }) => createElement("spinner" as any, props, children),
106
+ SpinnerMark: OpenTuiSpinnerMark as UiHost["SpinnerMark"],
75
107
  AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
76
108
  const resolvedColor = color ?? fg;
77
109
  const resolvedBackground = backgroundColor ?? bg;
@@ -11,7 +11,12 @@ import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history
11
11
  import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
12
12
  import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
13
13
  import { selectCachedResource } from "./cache";
14
- import { quoteWithFreshnessExchange, type CachedFinancialsSelection } from "./financials";
14
+ import {
15
+ hasDeepStatementHistory,
16
+ hasDetailedStatementRows,
17
+ quoteWithFreshnessExchange,
18
+ type CachedFinancialsSelection,
19
+ } from "./financials";
15
20
  import type { ProviderRouterCoreDeps } from "./route-types";
16
21
 
17
22
  export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
@@ -31,6 +36,10 @@ export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
31
36
  export class ProviderRouterBatchRoutes {
32
37
  constructor(private readonly deps: ProviderRouterBatchDeps) {}
33
38
 
39
+ private needsSingleFinancialsRoute(value: TickerFinancials): boolean {
40
+ return !(hasDetailedStatementRows(value) && hasDeepStatementHistory(value));
41
+ }
42
+
34
43
  async getQuotesBatch(
35
44
  targets: QuoteSubscriptionTarget[],
36
45
  options: { forceRefresh?: boolean } = {},
@@ -105,6 +114,7 @@ export class ProviderRouterBatchRoutes {
105
114
  ): Promise<TickerFinancialsBatchResult[]> {
106
115
  const forceRefresh = options.forceRefresh === true;
107
116
  const results = new Array<TickerFinancialsBatchResult | null>(targets.length).fill(null);
117
+ const batchFallbacks = new Array<TickerFinancials | null>(targets.length).fill(null);
108
118
  const misses: Array<{ index: number; target: CachedFinancialsTarget }> = [];
109
119
 
110
120
  targets.forEach((target, index) => {
@@ -139,6 +149,8 @@ export class ProviderRouterBatchRoutes {
139
149
  const entityKey = this.deps.getEntityKey(entry.target.symbol, entry.target.instrument ?? undefined);
140
150
  const variantKey = this.deps.getTickerVariantCandidates(entry.target.exchange)[0] ?? "";
141
151
  this.deps.cacheResource("financials", entityKey, variantKey, sourceKey, value, this.deps.resolveProviderPolicy("financials", batchProvider));
152
+ batchFallbacks[entry.index] = value;
153
+ if (this.needsSingleFinancialsRoute(value)) continue;
142
154
  results[entry.index] = { target: entry.target, financials: value };
143
155
  }
144
156
  }
@@ -153,7 +165,9 @@ export class ProviderRouterBatchRoutes {
153
165
  });
154
166
  results[index] = { target, financials };
155
167
  } catch (error) {
156
- results[index] = { target, financials: null, error };
168
+ results[index] = batchFallbacks[index]
169
+ ? { target, financials: batchFallbacks[index] }
170
+ : { target, financials: null, error };
157
171
  }
158
172
  }));
159
173
 
@@ -19,6 +19,7 @@ import { withBrokerTimeout } from "./brokers";
19
19
  import {
20
20
  deriveMarketCapFromShares,
21
21
  hasMeaningfulProfile,
22
+ hasShallowStatementHistory,
22
23
  mergeCachedFinancialRecords,
23
24
  mergeFinancials,
24
25
  quoteWithFreshnessExchange,
@@ -86,6 +87,10 @@ export class ProviderRouterFinancialRoutes {
86
87
  if (isOptionTicker && !cached.value.quote) {
87
88
  return quoteOnlyFinancials(cached.value);
88
89
  }
90
+ if (!cached.stale && hasMeaningfulProfile(cached.value) && hasShallowStatementHistory(cached.value)) {
91
+ const providerResult = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context);
92
+ return mergeFinancials(cached.value, providerResult?.value ?? null) ?? cached.value;
93
+ }
89
94
  if (!cached.stale && hasMeaningfulProfile(cached.value)) {
90
95
  return cached.value;
91
96
  }