gloomberb 0.9.3 → 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 (137) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/app/runtime/desktop-menu.ts +5 -1
  4. package/src/app/runtime/ticker-refresh.ts +4 -1
  5. package/src/app.tsx +10 -1
  6. package/src/brokers/account-cache.ts +1 -1
  7. package/src/brokers/sync-broker-instance.test.ts +107 -0
  8. package/src/brokers/sync-broker-instance.ts +22 -10
  9. package/src/cli/desktop-pane-shot.ts +2 -1
  10. package/src/cli/pane-functions/screenshot.ts +13 -1
  11. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  12. package/src/components/chart/bar-chart-renderer.ts +226 -27
  13. package/src/components/chart/core/pointer.ts +32 -6
  14. package/src/components/chart/price-axis-labels.tsx +3 -1
  15. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  16. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  17. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  18. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  19. package/src/components/chart/static/chart/surface.tsx +167 -24
  20. package/src/components/chart/stock-chart/index.test.ts +25 -0
  21. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  22. package/src/components/layout/detached-pane-shell.tsx +6 -3
  23. package/src/components/layout/floating-pane.tsx +3 -2
  24. package/src/components/layout/pane/content.tsx +20 -8
  25. package/src/components/layout/pane/footer/registration.tsx +6 -2
  26. package/src/components/layout/pane/frame.tsx +4 -4
  27. package/src/components/layout/pane/header.tsx +40 -4
  28. package/src/components/layout/pane/index.tsx +3 -1
  29. package/src/components/layout/pane/sizing.test.ts +43 -0
  30. package/src/components/layout/pane/sizing.ts +13 -4
  31. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  32. package/src/components/layout/shell/drag/runtime.ts +9 -2
  33. package/src/components/layout/shell/index.test.tsx +92 -2
  34. package/src/components/layout/shell/index.tsx +37 -2
  35. package/src/components/layout/shell/layout-state.test.ts +33 -0
  36. package/src/components/layout/shell/layout-state.ts +4 -9
  37. package/src/components/layout/shell/pane/layers.tsx +8 -2
  38. package/src/components/layout/shell/visible-layout.ts +17 -0
  39. package/src/components/layout/status-bar.test.tsx +6 -1
  40. package/src/components/layout/status-bar.tsx +5 -1
  41. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  42. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  43. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  44. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  45. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  46. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  47. package/src/components/table-view-shared.tsx +12 -4
  48. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  49. package/src/components/ui/data-table/types.ts +4 -0
  50. package/src/components/ui/fields.tsx +10 -3
  51. package/src/components/ui/page-stack-view.tsx +6 -2
  52. package/src/components/ui/table-layout.test.ts +25 -0
  53. package/src/components/ui/table-layout.ts +22 -6
  54. package/src/components/ui/tabs.tsx +26 -10
  55. package/src/components/ui/ui.test.tsx +87 -0
  56. package/src/core/state/app/layout-reducer.ts +2 -2
  57. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  58. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  59. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  60. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  61. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  62. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  63. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  64. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  65. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  66. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  67. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  68. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  69. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  70. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  71. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  72. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  73. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  74. package/src/plugins/builtin/options/view.test.tsx +141 -4
  75. package/src/plugins/builtin/options/view.tsx +76 -15
  76. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  77. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  78. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  79. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  80. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  81. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  82. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  83. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  84. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  85. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  86. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  87. package/src/plugins/catalog-ui.ts +2 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  89. package/src/plugins/ibkr/config.test.ts +31 -2
  90. package/src/plugins/ibkr/config.ts +9 -2
  91. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  92. package/src/plugins/ibkr/flex/index.ts +30 -0
  93. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  94. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  95. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  96. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  97. package/src/plugins/pane-manager/gridlock.ts +13 -5
  98. package/src/plugins/pane-manager/layout-state.ts +25 -0
  99. package/src/plugins/pane-manager.test.ts +33 -0
  100. package/src/plugins/pane-manager.ts +6 -1
  101. package/src/plugins/prediction-markets/chart.tsx +10 -4
  102. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  103. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  104. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  105. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  106. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  107. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  108. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  109. package/src/plugins/prediction-markets/pane.tsx +50 -22
  110. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  111. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  112. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  113. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  114. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  115. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  116. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  117. package/src/renderers/electrobun/view/styles.css +11 -0
  118. package/src/renderers/opentui/host.tsx +17 -1
  119. package/src/renderers/opentui/ui-host.tsx +57 -25
  120. package/src/sources/provider-router/batches.ts +16 -2
  121. package/src/sources/provider-router/financial-routes.ts +5 -0
  122. package/src/sources/provider-router/financials.ts +11 -23
  123. package/src/sources/provider-router/index.test.ts +189 -0
  124. package/src/sources/provider-router/primary.ts +3 -2
  125. package/src/sources/sec-edgar.test.ts +83 -0
  126. package/src/sources/sec-edgar.ts +231 -0
  127. package/src/sources/yahoo-finance.test.ts +51 -0
  128. package/src/sources/yahoo-finance.ts +49 -1
  129. package/src/state/app/context/index.test.tsx +5 -8
  130. package/src/types/broker.ts +1 -0
  131. package/src/types/config.ts +10 -0
  132. package/src/types/trading.ts +2 -0
  133. package/src/ui/host.tsx +16 -0
  134. package/src/ui/index.tsx +2 -0
  135. package/src/utils/financial-statements.ts +23 -0
  136. package/src/version.ts +1 -1
  137. 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
 
@@ -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);
@@ -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;
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  import type { CachedResourceRecord } from "../../data/resource-store";
2
2
  import type { AnalystResearchData, CorporateActionsData, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
3
3
  import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
4
+ import { mergeFinancialStatementRows } from "../../utils/financial-statements";
4
5
  import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
5
6
  import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
6
7
  import {
@@ -122,33 +123,20 @@ export function hasDetailedStatementRows(data: TickerFinancials | null | undefin
122
123
  return rows.some((row) => DETAILED_STATEMENT_KEYS.some((key) => typeof row[key] === "number"));
123
124
  }
124
125
 
125
- function mergeStatementRows(
126
- primaryRows: FinancialStatement[],
127
- fallbackRows: FinancialStatement[],
128
- ): FinancialStatement[] {
129
- if (primaryRows.length === 0) return fallbackRows;
130
- if (fallbackRows.length === 0) return primaryRows;
131
-
132
- const fallbackByDate = new Map(fallbackRows.map((row) => [row.date, row]));
133
- const primaryDates = new Set(primaryRows.map((row) => row.date));
134
- const mergedRows = primaryRows.map((row) => ({
135
- ...fallbackByDate.get(row.date),
136
- ...row,
137
- date: row.date,
138
- }));
139
-
140
- for (const row of fallbackRows) {
141
- if (!primaryDates.has(row.date)) mergedRows.push(row);
142
- }
126
+ export function hasDeepStatementHistory(data: TickerFinancials | null | undefined): boolean {
127
+ if (!data) return false;
128
+ return data.annualStatements.length >= 5 || data.quarterlyStatements.length >= 8;
129
+ }
143
130
 
144
- return mergedRows.sort((left, right) => left.date.localeCompare(right.date));
131
+ export function hasShallowStatementHistory(data: TickerFinancials | null | undefined): boolean {
132
+ return hasStatementRows(data) && !hasDeepStatementHistory(data);
145
133
  }
146
134
 
147
135
  export function mergeMissingStatementArrays(primary: TickerFinancials, fallback: TickerFinancials): TickerFinancials {
148
136
  return {
149
137
  ...primary,
150
- annualStatements: mergeStatementRows(primary.annualStatements, fallback.annualStatements),
151
- quarterlyStatements: mergeStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
138
+ annualStatements: mergeFinancialStatementRows(primary.annualStatements, fallback.annualStatements),
139
+ quarterlyStatements: mergeFinancialStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
152
140
  };
153
141
  }
154
142
 
@@ -216,8 +204,8 @@ export function mergeFinancials(primary: TickerFinancials | null, fallback: Tick
216
204
  profile: mergeDefinedObject(primary.profile, fallback.profile),
217
205
  fundamentals: mergeDefinedObject(primary.fundamentals, fallback.fundamentals),
218
206
  priceHistory: normalizePriceHistory(dominant.priceHistory.length > 0 ? dominant.priceHistory : secondary.priceHistory),
219
- annualStatements: mergeStatementRows(primary.annualStatements, fallback.annualStatements),
220
- quarterlyStatements: mergeStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
207
+ annualStatements: mergeFinancialStatementRows(primary.annualStatements, fallback.annualStatements),
208
+ quarterlyStatements: mergeFinancialStatementRows(primary.quarterlyStatements, fallback.quarterlyStatements),
221
209
  });
222
210
  }
223
211