gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Glob } from "bun";
3
+ import * as realSurfaceSync from "../../../../components/chart/native/surface/sync";
4
+ import * as realKittySupport from "../../../../components/chart/native/kitty/support";
5
+ import { NativeSurfaceManager as StubNativeSurfaceManager } from "./chart/surface-manager";
6
+ import * as stubSurfaceSync from "./chart/surface-sync";
7
+ import * as stubKittySupport from "./chart/kitty-support";
8
+
9
+ // The desktop view bundle aliases these modules to the stubs in this directory
10
+ // (see build-assets.ts COMMON_ALIAS_RULES). A method the app calls but the stub
11
+ // lacks is a desktop startup crash, not a type error, so check the shapes here.
12
+
13
+ function exportedFunctions(module: Record<string, unknown>): string[] {
14
+ return Object.keys(module)
15
+ .filter((name) => typeof module[name] === "function")
16
+ .sort();
17
+ }
18
+
19
+ async function calledSurfaceManagerMethods(): Promise<string[]> {
20
+ const calls = new Set<string>();
21
+ const pattern = /\b\w*[sS]urfaceManager(?:\.current)?\.([a-zA-Z]\w*)\s*\(/g;
22
+ for await (const path of new Glob("src/**/*.{ts,tsx}").scan(".")) {
23
+ if (path.includes("native-stubs") || path.includes("native/surface/manager")) continue;
24
+ for (const match of (await Bun.file(path).text()).matchAll(pattern)) {
25
+ calls.add(match[1]!);
26
+ }
27
+ }
28
+ return [...calls].sort();
29
+ }
30
+
31
+ describe("desktop view native stubs", () => {
32
+ test("surface manager stub answers every method the app calls on it", async () => {
33
+ const called = await calledSurfaceManagerMethods();
34
+ expect(called.length).toBeGreaterThan(3);
35
+ const stub = StubNativeSurfaceManager.prototype as unknown as Record<string, unknown>;
36
+ expect(called.filter((name) => typeof stub[name] !== "function")).toEqual([]);
37
+ });
38
+
39
+ test("module stubs export every function of the modules they replace", () => {
40
+ expect(
41
+ exportedFunctions(realSurfaceSync).filter((name) => !exportedFunctions(stubSurfaceSync).includes(name)),
42
+ ).toEqual([]);
43
+ expect(
44
+ exportedFunctions(realKittySupport).filter((name) => !exportedFunctions(stubKittySupport).includes(name)),
45
+ ).toEqual([]);
46
+ });
47
+ });
@@ -4,6 +4,7 @@ import type { DataProvider, QuoteSubscriptionTarget } from "../../../../types/da
4
4
  import type { Quote, TickerFinancials } from "../../../../types/financials";
5
5
  import { backendRequest, getElectrobunBackendInitSnapshot, onCapabilityEvent } from "../backend-rpc";
6
6
  import { createCapabilityInvoker } from "./capability-invoker";
7
+ import { RemoteQuoteSubscriptionRegistry } from "./quote-subscription-registry";
7
8
 
8
9
  const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
9
10
  const NEWS_CAPABILITY_ID = "news.core";
@@ -75,17 +76,10 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
75
76
  const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID);
76
77
  const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID);
77
78
  let nextSubscriptionId = 1;
78
- const quoteListeners = new Map<string, {
79
- target: QuoteSubscriptionTarget;
80
- listeners: Set<(target: QuoteSubscriptionTarget, quote: Quote) => void>;
81
- }>();
82
79
  let quoteBackendSubscriptionId: string | null = null;
83
80
  let disposeQuoteBackendMessages: (() => void) | null = null;
84
81
  let quoteBackendFlushTimer: ReturnType<typeof setTimeout> | null = null;
85
82
 
86
- const quoteTargetKey = (target: QuoteSubscriptionTarget) =>
87
- `${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? target.context?.instrument?.symbol ?? ""}`;
88
-
89
83
  const scheduleQuoteBackendSubscriptionFlush = () => {
90
84
  if (quoteBackendFlushTimer) return;
91
85
  quoteBackendFlushTimer = setTimeout(() => {
@@ -94,9 +88,11 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
94
88
  }, 25);
95
89
  };
96
90
 
91
+ const quoteSubscriptions = new RemoteQuoteSubscriptionRegistry(scheduleQuoteBackendSubscriptionFlush);
92
+
97
93
  const flushQuoteBackendSubscription = () => {
98
94
  if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return;
99
- const targets = [...quoteListeners.values()].map((entry) => entry.target);
95
+ const targets = quoteSubscriptions.backendTargets();
100
96
  const previousSubscriptionId = quoteBackendSubscriptionId;
101
97
  if (previousSubscriptionId) {
102
98
  quoteBackendSubscriptionId = null;
@@ -110,10 +106,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
110
106
  quoteBackendSubscriptionId = subscriptionId;
111
107
  disposeQuoteBackendMessages = onCapabilityEvent(subscriptionId, (message) => {
112
108
  const event = message.event as { target: QuoteSubscriptionTarget; quote: Quote };
113
- const key = quoteTargetKey(event.target);
114
- for (const listener of quoteListeners.get(key)?.listeners ?? []) {
115
- listener(event.target, event.quote);
116
- }
109
+ quoteSubscriptions.dispatch(event.target, event.quote);
117
110
  });
118
111
  void backendRequest("capability.subscribe", {
119
112
  subscriptionId,
@@ -145,38 +138,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
145
138
  ),
146
139
  subscribeQuotes: (targets, onQuote) => {
147
140
  if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return () => {};
148
- const uniqueTargets = [...new Map(
149
- targets.map((target) => [
150
- quoteTargetKey(target),
151
- target,
152
- ] as const),
153
- ).values()];
154
- if (uniqueTargets.length === 0) return () => {};
155
-
156
- let disposed = false;
157
- for (const target of uniqueTargets) {
158
- const key = quoteTargetKey(target);
159
- const entry = quoteListeners.get(key) ?? { target, listeners: new Set() };
160
- entry.target = target;
161
- entry.listeners.add(onQuote);
162
- quoteListeners.set(key, entry);
163
- }
164
- scheduleQuoteBackendSubscriptionFlush();
165
-
166
- return () => {
167
- if (disposed) return;
168
- disposed = true;
169
- for (const target of uniqueTargets) {
170
- const key = quoteTargetKey(target);
171
- const entry = quoteListeners.get(key);
172
- if (!entry) continue;
173
- entry.listeners.delete(onQuote);
174
- if (entry.listeners.size === 0) {
175
- quoteListeners.delete(key);
176
- }
177
- }
178
- scheduleQuoteBackendSubscriptionFlush();
179
- };
141
+ return quoteSubscriptions.subscribe(targets, onQuote);
180
142
  },
181
143
  };
182
144
 
@@ -16,7 +16,9 @@ class RemoteSessionStore {
16
16
  schemaVersion: number;
17
17
  }>({
18
18
  delayMs: SESSION_SAVE_DEBOUNCE_MS,
19
- save: ({ sessionId, value, schemaVersion }) => backendRequest("session.set", { sessionId, value, schemaVersion }),
19
+ save: async ({ sessionId, value, schemaVersion }) => {
20
+ await backendRequest("session.set", { sessionId, value, schemaVersion });
21
+ },
20
22
  });
21
23
 
22
24
  get<T>(sessionId = "app", schemaVersion = 1) {
@@ -138,7 +140,9 @@ class RemotePluginStateStore {
138
140
  this.pendingBackendSaves.clear();
139
141
  const save = this.backendSaveInFlight
140
142
  .catch(() => {})
141
- .then(() => backendRequest<void>("pluginState.setMany", { entries }))
143
+ .then(async () => {
144
+ await backendRequest("pluginState.setMany", { entries });
145
+ })
142
146
  .catch(() => {});
143
147
  this.backendSaveInFlight = save;
144
148
  return save;
@@ -0,0 +1,86 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
3
+ import type { Quote } from "../../../../types/financials";
4
+ import { RemoteQuoteSubscriptionRegistry } from "./quote-subscription-registry";
5
+
6
+ const lowPriorityTarget: QuoteSubscriptionTarget = {
7
+ symbol: "TSLA",
8
+ exchange: "NASDAQ",
9
+ surface: "watchlist",
10
+ visible: true,
11
+ selected: false,
12
+ weight: 10,
13
+ };
14
+
15
+ const highPriorityTarget: QuoteSubscriptionTarget = {
16
+ ...lowPriorityTarget,
17
+ surface: "detail",
18
+ selected: true,
19
+ weight: 100,
20
+ };
21
+
22
+ const quote: Quote = {
23
+ symbol: "TSLA",
24
+ price: 250,
25
+ currency: "USD",
26
+ change: 5,
27
+ changePercent: 2,
28
+ lastUpdated: 1_700_000_000_000,
29
+ };
30
+
31
+ describe("RemoteQuoteSubscriptionRegistry", () => {
32
+ test("recomputes the backend target and listener set when an overlapping subscription leaves", () => {
33
+ let changes = 0;
34
+ const lowEvents: Array<{ target: QuoteSubscriptionTarget; quote: Quote }> = [];
35
+ const highEvents: Array<{ target: QuoteSubscriptionTarget; quote: Quote }> = [];
36
+ const registry = new RemoteQuoteSubscriptionRegistry(() => { changes += 1; });
37
+
38
+ const unsubscribeLow = registry.subscribe([lowPriorityTarget], (target, event) => lowEvents.push({ target, quote: event }));
39
+ const unsubscribeHigh = registry.subscribe([highPriorityTarget], (target, event) => highEvents.push({ target, quote: event }));
40
+
41
+ expect(registry.backendTargets()).toEqual([highPriorityTarget]);
42
+ registry.dispatch(highPriorityTarget, quote);
43
+ expect(lowEvents).toEqual([{ target: lowPriorityTarget, quote }]);
44
+ expect(highEvents).toEqual([{ target: highPriorityTarget, quote }]);
45
+
46
+ unsubscribeHigh();
47
+ expect(registry.backendTargets()).toEqual([lowPriorityTarget]);
48
+ registry.dispatch(lowPriorityTarget, quote);
49
+ expect(lowEvents).toEqual([
50
+ { target: lowPriorityTarget, quote },
51
+ { target: lowPriorityTarget, quote },
52
+ ]);
53
+ expect(highEvents).toEqual([{ target: highPriorityTarget, quote }]);
54
+
55
+ unsubscribeLow();
56
+ expect(registry.backendTargets()).toEqual([]);
57
+ expect(changes).toBe(4);
58
+ });
59
+
60
+ test("preserves options stream scope across the desktop backend bridge", () => {
61
+ const target: QuoteSubscriptionTarget = {
62
+ symbol: "AAPL260731C00110000",
63
+ exchange: "OPTIONS",
64
+ surface: "options",
65
+ visible: true,
66
+ selected: true,
67
+ weight: 100,
68
+ };
69
+ const optionQuote: Quote = {
70
+ ...quote,
71
+ symbol: target.symbol,
72
+ price: 2.5,
73
+ bid: 2.45,
74
+ ask: 2.55,
75
+ dataSource: "live",
76
+ };
77
+ const events: Quote[] = [];
78
+ const registry = new RemoteQuoteSubscriptionRegistry();
79
+
80
+ registry.subscribe([target], (_eventTarget, event) => events.push(event));
81
+
82
+ expect(registry.backendTargets()).toEqual([target]);
83
+ registry.dispatch(target, optionQuote);
84
+ expect(events).toEqual([optionQuote]);
85
+ });
86
+ });
@@ -0,0 +1,72 @@
1
+ import { mergeQuoteSubscriptionTargets } from "../../../../market-data/quote-subscription-target";
2
+ import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
3
+ import type { Quote } from "../../../../types/financials";
4
+
5
+ export type RemoteQuoteListener = (target: QuoteSubscriptionTarget, quote: Quote) => void;
6
+
7
+ interface RemoteQuoteSubscriptionEntry {
8
+ target: QuoteSubscriptionTarget;
9
+ listener: RemoteQuoteListener;
10
+ }
11
+
12
+ function quoteTargetKey(target: QuoteSubscriptionTarget): string {
13
+ return `${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? target.context?.instrument?.symbol ?? ""}`;
14
+ }
15
+
16
+ export class RemoteQuoteSubscriptionRegistry {
17
+ private nextSubscriptionId = 1;
18
+ private readonly entriesByTarget = new Map<string, Map<number, RemoteQuoteSubscriptionEntry>>();
19
+
20
+ constructor(private readonly onChange: () => void = () => {}) {}
21
+
22
+ subscribe(targets: QuoteSubscriptionTarget[], listener: RemoteQuoteListener): () => void {
23
+ const groupedTargets = new Map<string, QuoteSubscriptionTarget[]>();
24
+ for (const target of targets) {
25
+ const key = quoteTargetKey(target);
26
+ const group = groupedTargets.get(key) ?? [];
27
+ group.push(target);
28
+ groupedTargets.set(key, group);
29
+ }
30
+ if (groupedTargets.size === 0) return () => {};
31
+
32
+ const subscriptionId = this.nextSubscriptionId++;
33
+ for (const [key, candidates] of groupedTargets) {
34
+ const target = mergeQuoteSubscriptionTargets(candidates);
35
+ if (!target) continue;
36
+ const entries = this.entriesByTarget.get(key) ?? new Map();
37
+ entries.set(subscriptionId, { target, listener });
38
+ this.entriesByTarget.set(key, entries);
39
+ }
40
+ this.onChange();
41
+
42
+ let disposed = false;
43
+ return () => {
44
+ if (disposed) return;
45
+ disposed = true;
46
+ for (const key of groupedTargets.keys()) {
47
+ const entries = this.entriesByTarget.get(key);
48
+ if (!entries) continue;
49
+ entries.delete(subscriptionId);
50
+ if (entries.size === 0) this.entriesByTarget.delete(key);
51
+ }
52
+ this.onChange();
53
+ };
54
+ }
55
+
56
+ backendTargets(): QuoteSubscriptionTarget[] {
57
+ const targets: QuoteSubscriptionTarget[] = [];
58
+ for (const entries of this.entriesByTarget.values()) {
59
+ const target = mergeQuoteSubscriptionTargets(
60
+ [...entries.values()].map((entry) => entry.target),
61
+ );
62
+ if (target) targets.push(target);
63
+ }
64
+ return targets;
65
+ }
66
+
67
+ dispatch(target: QuoteSubscriptionTarget, quote: Quote): void {
68
+ for (const entry of this.entriesByTarget.get(quoteTargetKey(target))?.values() ?? []) {
69
+ entry.listener(entry.target, quote);
70
+ }
71
+ }
72
+ }
@@ -3,11 +3,11 @@ import { backendRequest } from "../backend-rpc";
3
3
 
4
4
  export class RemoteTickerRepository {
5
5
  async loadAllTickers(): Promise<TickerRecord[]> {
6
- return backendRequest<TickerRecord[]>("ticker.loadAll");
6
+ return backendRequest("ticker.loadAll");
7
7
  }
8
8
 
9
9
  async loadTicker(symbol: string): Promise<TickerRecord | null> {
10
- return backendRequest<TickerRecord | null>("ticker.load", { symbol });
10
+ return backendRequest("ticker.load", { symbol });
11
11
  }
12
12
 
13
13
  async saveTicker(ticker: TickerRecord): Promise<void> {
@@ -1,4 +1,3 @@
1
- /// <reference lib="dom" />
2
1
  /** @jsxImportSource react */
3
2
  import { useCallback, useMemo, useState, type CSSProperties, type ReactNode } from "react";
4
3
  import { colors } from "../../../theme/colors";
@@ -1,4 +1,3 @@
1
- /// <reference lib="dom" />
2
1
  /** @jsxImportSource react */
3
2
  import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
4
3
  import type { RendererHost, UiHost } from "../../../ui/host";
@@ -190,7 +189,7 @@ export const webRendererHost: RendererHost = {
190
189
  try {
191
190
  return await navigator.clipboard.readText();
192
191
  } catch {
193
- return await backendRequest<string>("host.readText");
192
+ return await backendRequest("host.readText");
194
193
  }
195
194
  },
196
195
  notify(notification) {
@@ -9,7 +9,7 @@ import { backendRequest, onUpdateProgress } from "./backend-rpc";
9
9
  export function installElectrobunUpdateHost(): void {
10
10
  setUpdateHost({
11
11
  checkForUpdateDetailed(currentVersion: string): Promise<UpdateCheckResult> {
12
- return backendRequest<UpdateCheckResult>("update.check", { currentVersion });
12
+ return backendRequest("update.check", { currentVersion });
13
13
  },
14
14
  performUpdate(release: ReleaseInfo, onProgress: (progress: UpdateProgress) => void): Promise<void> {
15
15
  return new Promise((resolve) => {
@@ -96,7 +96,8 @@ export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(functi
96
96
  const surfaceId = useRef(`opentui-chart:${nextChartSurfaceId++}`).current;
97
97
  const renderableRef = useRef<NativeRenderableNode | null>(null);
98
98
  const [target, setTarget] = useState<SurfaceTarget | null>(null);
99
- const nativeBitmap = (bitmaps?.[0] ?? bitmap ?? null) as NativeChartBitmap | null;
99
+ const firstBitmap = Array.isArray(bitmaps) ? bitmaps[0] : null;
100
+ const nativeBitmap = (firstBitmap ?? bitmap ?? null) as NativeChartBitmap | null;
100
101
  const nativeCrosshair = (crosshair ?? null) as ChartCrosshairOverlay | null;
101
102
  const nativeBitmapKey = useMemo(() => (nativeBitmap ? bitmapKey(nativeBitmap) : null), [nativeBitmap]);
102
103
 
@@ -2,6 +2,10 @@ import type { ReactNode } from "react";
2
2
  import { useMemo } from "react";
3
3
  import {
4
4
  DialogProvider as OpenTuiDialogProvider,
5
+ type AlertContext as OpenTuiAlertContext,
6
+ type AlertOptions as OpenTuiAlertOptions,
7
+ type PromptContext as OpenTuiPromptContext,
8
+ type PromptOptions as OpenTuiPromptOptions,
5
9
  useDialog as useOpenTuiDialog,
6
10
  useDialogState as useOpenTuiDialogState,
7
11
  } from "@opentui-ui/dialog/react";
@@ -45,9 +49,21 @@ function OpenTuiDialogBridge({ children }: { children: ReactNode }) {
45
49
  const openTuiDialog = useOpenTuiDialog();
46
50
  const isOpen = useOpenTuiDialogState((state) => state.isOpen);
47
51
  const dialog = useMemo<DialogApi>(() => {
48
- const wrapOptions = (options: Record<string, unknown>) => ({
52
+ const wrapAlertOptions = (options: Record<string, unknown>): OpenTuiAlertOptions => ({
49
53
  ...options,
50
- content: (context: Record<string, unknown>) => {
54
+ content: (context: OpenTuiAlertContext) => {
55
+ const dialogId = normalizeDialogId(context.dialogId);
56
+ return (
57
+ <OpenTuiDialogContentBridge dialog={dialog} dialogId={dialogId}>
58
+ {renderDialogContent(options.content, { ...context, dialogId })}
59
+ </OpenTuiDialogContentBridge>
60
+ );
61
+ },
62
+ });
63
+
64
+ const wrapPromptOptions = <T,>(options: Record<string, unknown>): OpenTuiPromptOptions<T> => ({
65
+ ...options,
66
+ content: (context: OpenTuiPromptContext<T>) => {
51
67
  const dialogId = normalizeDialogId(context.dialogId);
52
68
  return (
53
69
  <OpenTuiDialogContentBridge dialog={dialog} dialogId={dialogId}>
@@ -58,8 +74,8 @@ function OpenTuiDialogBridge({ children }: { children: ReactNode }) {
58
74
  });
59
75
 
60
76
  return {
61
- alert: (options) => openTuiDialog.alert(wrapOptions(options)),
62
- prompt: (options) => openTuiDialog.prompt(wrapOptions(options)),
77
+ alert: (options) => openTuiDialog.alert(wrapAlertOptions(options)),
78
+ prompt: <T,>(options: Record<string, unknown>) => openTuiDialog.prompt<T>(wrapPromptOptions<T>(options)),
63
79
  };
64
80
  }, [openTuiDialog]);
65
81
 
@@ -7,13 +7,22 @@ export interface JimpImageLike {
7
7
  bitmap: {
8
8
  width: number;
9
9
  height: number;
10
- data: Uint8Array;
10
+ data: ArrayLike<number>;
11
11
  };
12
12
  clone(): JimpImageLike;
13
13
  contain(options: { w: number; h: number }): JimpImageLike;
14
14
  cover(options: { w: number; h: number }): JimpImageLike;
15
15
  }
16
16
 
17
+ function adaptJimpImage(image: JimpImageLike): JimpImageLike {
18
+ return {
19
+ bitmap: image.bitmap,
20
+ clone: () => adaptJimpImage(image.clone()),
21
+ contain: (options) => adaptJimpImage(image.contain(options)),
22
+ cover: (options) => adaptJimpImage(image.cover(options)),
23
+ };
24
+ }
25
+
17
26
  interface ImageBitmapOptions {
18
27
  width: number;
19
28
  height: number;
@@ -58,7 +67,8 @@ function loadSourceImage(src: string): Promise<JimpImageLike> {
58
67
  if (cached) return cached;
59
68
 
60
69
  const promise = fetchImageBytes(src)
61
- .then((bytes) => Jimp.read(bytes) as Promise<JimpImageLike>)
70
+ .then((bytes) => Jimp.read(bytes))
71
+ .then(adaptJimpImage)
62
72
  .catch((error) => {
63
73
  sourceImageCache.delete(src);
64
74
  throw error;
@@ -1,74 +1,32 @@
1
- import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
1
+ import { useMemo, type ReactNode } from "react";
2
2
  import { useNativeRenderer } from "../../ui";
3
3
  import {
4
+ createShortcutRegistry,
4
5
  InputHostProvider,
5
- shouldDeliverShortcut,
6
+ useRegisteredShortcut,
6
7
  type InputHost,
7
- type KeyEventLike,
8
- type ShortcutOptions,
9
8
  } from "../../react/input";
10
9
  import { toKeyEventLike, useKeyboard, useTerminalDimensions } from "./host";
11
10
 
12
- interface ShortcutEntry {
13
- handlerRef: { current: (event: KeyEventLike) => void };
14
- enabledRef: { current: boolean };
15
- allowEditableRef: { current: boolean };
16
- phase: NonNullable<ShortcutOptions["phase"]>;
17
- order: number;
18
- }
19
-
20
- let nextShortcutOrder = 1;
21
-
22
11
  export function OpenTuiInputHostProvider({ children }: { children: ReactNode }) {
23
12
  const renderer = useNativeRenderer();
24
- const shortcutsRef = useRef<ShortcutEntry[]>([]);
25
- const dispatchShortcut = (event: KeyEventLike) => {
26
- const shortcuts = shortcutsRef.current;
27
- for (const phase of ["before", "normal", "after"] as const) {
28
- if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
29
- for (const entry of shortcuts) {
30
- if (entry.phase !== phase || !entry.enabledRef.current) continue;
31
- if (!shouldDeliverShortcut(event, entry.allowEditableRef.current)) continue;
32
- entry.handlerRef.current(event);
33
- if (event.propagationStopped) return;
34
- }
35
- }
36
- };
13
+ const shortcutRegistry = useMemo(() => createShortcutRegistry(), []);
37
14
 
38
15
  useKeyboard((event) => {
39
16
  const shortcutEvent = toKeyEventLike(event);
40
17
  shortcutEvent.targetEditable = renderer.currentFocusedEditor != null;
41
- dispatchShortcut(shortcutEvent);
18
+ shortcutRegistry.dispatch(shortcutEvent);
42
19
  });
43
20
 
44
21
  const host = useMemo<InputHost>(() => ({
45
22
  useShortcut(handler, options) {
46
- const handlerRef = useRef(handler);
47
- const enabledRef = useRef(options?.enabled !== false);
48
- const allowEditableRef = useRef(options?.allowEditable === true);
49
- handlerRef.current = handler;
50
- enabledRef.current = options?.enabled !== false;
51
- allowEditableRef.current = options?.allowEditable === true;
52
-
53
- useLayoutEffect(() => {
54
- const entry: ShortcutEntry = {
55
- handlerRef,
56
- enabledRef,
57
- allowEditableRef,
58
- phase: options?.phase ?? "normal",
59
- order: nextShortcutOrder++,
60
- };
61
- shortcutsRef.current = [...shortcutsRef.current, entry].sort((a, b) => a.order - b.order);
62
- return () => {
63
- shortcutsRef.current = shortcutsRef.current.filter((current) => current !== entry);
64
- };
65
- }, [options?.phase, options?.scope]);
23
+ useRegisteredShortcut(shortcutRegistry, handler, options);
66
24
  },
67
25
  useViewport() {
68
26
  const dimensions = useTerminalDimensions();
69
27
  return { width: dimensions.width, height: dimensions.height };
70
28
  },
71
- }), []);
29
+ }), [shortcutRegistry]);
72
30
 
73
31
  return (
74
32
  <InputHostProvider host={host}>
@@ -24,6 +24,7 @@ import { createPiAiHost } from "../../plugins/builtin/ai/pi";
24
24
  import {
25
25
  installAiRunHost,
26
26
  } from "../../plugins/builtin/ai/runner";
27
+ import { createAppServices } from "../../core/app-services";
27
28
 
28
29
  const AI_STARTUP_READINESS_TIMEOUT_MS = 5_000;
29
30
 
@@ -140,6 +141,7 @@ export async function startOpenTuiApp(options: StartOpenTuiAppOptions = {}): Pro
140
141
  >
141
142
  <App
142
143
  config={config}
144
+ servicesFactory={createAppServices}
143
145
  externalPlugins={externalPlugins}
144
146
  cliLaunchRequest={cliLaunchRequest}
145
147
  remoteControlAdapter={remoteControlAdapter}
@@ -0,0 +1,44 @@
1
+ import type { PricePoint } from "../../types/financials";
2
+ import { getPricePointTimestamp } from "../../utils/price-history";
3
+
4
+ const MAX_NEIGHBOR_GAP_MS = 60 * 60 * 1000;
5
+ const STABLE_NEIGHBOR_CLOSE_RATIO = 0.01;
6
+ const ISOLATED_OHLC_OUTLIER_RATIO = 0.04;
7
+
8
+ function relativeDifference(value: number, reference: number): number {
9
+ return Math.abs(value - reference) / reference;
10
+ }
11
+
12
+ function isIsolatedOhlcOutlier(
13
+ previous: PricePoint,
14
+ point: PricePoint,
15
+ next: PricePoint,
16
+ ): boolean {
17
+ const previousGap = getPricePointTimestamp(point) - getPricePointTimestamp(previous);
18
+ const nextGap = getPricePointTimestamp(next) - getPricePointTimestamp(point);
19
+ if (
20
+ previousGap <= 0
21
+ || nextGap <= 0
22
+ || previousGap > MAX_NEIGHBOR_GAP_MS
23
+ || nextGap > MAX_NEIGHBOR_GAP_MS
24
+ || relativeDifference(previous.close, point.close) > STABLE_NEIGHBOR_CLOSE_RATIO
25
+ || relativeDifference(next.close, point.close) > STABLE_NEIGHBOR_CLOSE_RATIO
26
+ ) {
27
+ return false;
28
+ }
29
+
30
+ return [point.open, point.high, point.low].some((value) => (
31
+ typeof value === "number"
32
+ && Number.isFinite(value)
33
+ && relativeDifference(value, point.close) > ISOLATED_OHLC_OUTLIER_RATIO
34
+ ));
35
+ }
36
+
37
+ export function hasMalformedCloudIntradayHistory(points: readonly PricePoint[]): boolean {
38
+ for (let index = 1; index < points.length - 1; index += 1) {
39
+ if (isIsolatedOhlcOutlier(points[index - 1]!, points[index]!, points[index + 1]!)) {
40
+ return true;
41
+ }
42
+ }
43
+ return false;
44
+ }