gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,15 +1,18 @@
1
- import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
2
- import { useKeyboard, useRenderer } from "@opentui/react";
1
+ import { Box, ChartSurface, ScrollBox, Text } from "../../ui";
2
+ import { TextAttributes, type BoxRenderable, type NativeRendererHost as CliRenderer } from "../../ui";
3
+ import { useNativeRenderer, useUiCapabilities } from "../../ui";
4
+ import { useShortcut } from "../../react/input";
3
5
  import { memo, useEffect, useMemo, useRef, useState } from "react";
4
6
  import { useChartQueries } from "../../market-data/hooks";
5
7
  import { buildChartKey } from "../../market-data/selectors";
6
- import { useAppState, usePaneSettingValue } from "../../state/app-context";
8
+ import { useAppSelector, usePaneSettingValue } from "../../state/app-context";
9
+ import { usePaneFooter, type PaneHint } from "../layout/pane-footer";
7
10
  import { blendHex, colors, getComparisonSeriesColor, priceColor } from "../../theme/colors";
8
11
  import type { BrokerContractRef } from "../../types/instrument";
9
12
  import type { PricePoint } from "../../types/financials";
10
13
  import { formatPercentRaw } from "../../utils/format";
11
14
  import { formatMarketPriceWithCurrency, formatSignedMarketPrice } from "../../utils/market-format";
12
- import { getSharedDataProvider } from "../../plugins/registry";
15
+ import { getSharedMarketData } from "../../plugins/registry";
13
16
  import {
14
17
  applyComparisonZoomAroundAnchor,
15
18
  getMaxComparisonPanOffset,
@@ -91,7 +94,6 @@ import { resolveChartRendererState } from "./native/renderer-selection";
91
94
  import { getNativeSurfaceManager } from "./native/surface-manager";
92
95
  import { syncCachedNativeSurface } from "./native/surface-sync";
93
96
  import { formatAxisCell, formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
94
- import { ChartControlHint } from "./chart-control-hint";
95
97
 
96
98
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
97
99
  area: "A",
@@ -552,7 +554,8 @@ function ComparisonStockChartView({
552
554
  symbolSources,
553
555
  onOpenSymbol,
554
556
  }: ComparisonStockChartViewProps) {
555
- const renderer = useRenderer();
557
+ const renderer = useNativeRenderer();
558
+ const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
556
559
  const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
557
560
  const [storedRangePreset] = usePaneSettingValue("rangePreset", DEFAULT_COMPARISON_CHART_RANGE_PRESET);
558
561
  const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_COMPARISON_CHART_RESOLUTION);
@@ -573,6 +576,7 @@ function ComparisonStockChartView({
573
576
  const supportMap = useMemo(() => buildChartResolutionSupportMap(resolutionSupport ?? []), [resolutionSupport]);
574
577
  const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
575
578
  const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
579
+ const [canvasBaseBitmapState, setCanvasBaseBitmapState] = useState<{ key: string; bitmap: NativeChartBitmap } | null>(null);
576
580
  const plotRef = useRef<BoxRenderable | null>(null);
577
581
  const nativeBaseSurfaceIdRef = useRef(`comparison-chart-surface:${paneId}:base`);
578
582
  const nativeCrosshairSurfaceIdRef = useRef(`comparison-chart-surface:${paneId}:crosshair`);
@@ -580,6 +584,7 @@ function ComparisonStockChartView({
580
584
  const lastNativeGeometryRef = useRef<{ rect: CellRect; visibleRect: CellRect | null } | null>(null);
581
585
  const lastNativeBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
582
586
  const lastNativeCrosshairBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
587
+ const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
583
588
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
584
589
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
585
590
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
@@ -596,7 +601,7 @@ function ComparisonStockChartView({
596
601
  const headerRows = 1;
597
602
  const controlRows = 2;
598
603
  const timeAxisRows = 1;
599
- const helpRows = 1;
604
+ const helpRows = 0;
600
605
  const legendColumns = getLegendColumns(width);
601
606
  const legendNeededRows = symbols.length > 0 ? Math.ceil(symbols.length / legendColumns) : 0;
602
607
  const legendRows = legendNeededRows > 0
@@ -734,7 +739,7 @@ function ComparisonStockChartView({
734
739
  ), [nativeSurfaceManager]);
735
740
 
736
741
  useEffect(() => {
737
- const provider = getSharedDataProvider();
742
+ const provider = getSharedMarketData();
738
743
  if ((!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) || symbolSources.length === 0) {
739
744
  setResolutionSupport(null);
740
745
  return;
@@ -1009,13 +1014,19 @@ function ComparisonStockChartView({
1009
1014
  setViewState(nextState);
1010
1015
  };
1011
1016
 
1017
+ const projectionViewState = useMemo(() => ({
1018
+ panOffset: viewState.panOffset,
1019
+ zoomLevel: viewState.zoomLevel,
1020
+ renderMode: viewState.renderMode,
1021
+ }), [viewState.panOffset, viewState.renderMode, viewState.zoomLevel]);
1022
+
1012
1023
  const visibleWindow = useMemo(() => (
1013
- getVisibleComparisonWindow(series, viewState, chartWidth)
1014
- ), [chartWidth, series, viewState]);
1024
+ getVisibleComparisonWindow(series, projectionViewState, chartWidth)
1025
+ ), [chartWidth, projectionViewState, series]);
1015
1026
 
1016
1027
  const projection = useMemo(() => (
1017
- projectComparisonChartData(series, chartWidth, viewState, axisMode)
1018
- ), [axisMode, chartWidth, series, viewState]);
1028
+ projectComparisonChartData(series, chartWidth, projectionViewState, axisMode)
1029
+ ), [axisMode, chartWidth, projectionViewState, series]);
1019
1030
 
1020
1031
  const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
1021
1032
  const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
@@ -1054,6 +1065,8 @@ function ComparisonStockChartView({
1054
1065
 
1055
1066
  const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
1056
1067
  const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
1068
+ const useCanvasChart = canvasCharts && effectiveRenderer !== "kitty";
1069
+ const showNativeUnavailable = rendererState.nativeUnavailable && !useCanvasChart;
1057
1070
 
1058
1071
  const staticScene = useMemo(() => buildComparisonChartScene(projection, {
1059
1072
  width: chartWidth,
@@ -1074,7 +1087,7 @@ function ComparisonStockChartView({
1074
1087
  }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1075
1088
 
1076
1089
  const interactiveResult = useMemo(() => (
1077
- effectiveRenderer === "kitty"
1090
+ effectiveRenderer === "kitty" || useCanvasChart
1078
1091
  ? null
1079
1092
  : renderComparisonChart(projection, {
1080
1093
  width: chartWidth,
@@ -1084,9 +1097,66 @@ function ComparisonStockChartView({
1084
1097
  selectedSymbol: viewState.selectedSymbol,
1085
1098
  colors: chartColors,
1086
1099
  })
1087
- ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, viewState.selectedSymbol]);
1100
+ ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, useCanvasChart, viewState.selectedSymbol]);
1101
+
1102
+ const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
1103
+ const timeAxisLabel = result.timeLabels || staticResult.timeLabels;
1104
+ const footerHints = useMemo<PaneHint[]>(() => {
1105
+ const zoomIn = () => {
1106
+ setViewState((current) => applyComparisonZoomAroundAnchor(
1107
+ clearActivePreset(current),
1108
+ current.zoomLevel * 1.5,
1109
+ RIGHT_EDGE_ANCHOR_RATIO,
1110
+ series,
1111
+ ));
1112
+ };
1113
+ const resetView = () => {
1114
+ pendingCanonicalResetRef.current += 1;
1115
+ updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1116
+ setViewState((current) => ({
1117
+ ...(resolveResolutionSelection(current, effectiveResolution, supportMap, visibleDateWindow) ?? current),
1118
+ panOffset: 0,
1119
+ zoomLevel: 1,
1120
+ cursorX: null,
1121
+ cursorY: null,
1122
+ }));
1123
+ };
1124
+ const cycleRange = () => {
1125
+ const currentIndex = TIME_RANGES.indexOf(viewState.presetRange);
1126
+ setRangePreset(TIME_RANGES[(currentIndex + 1) % TIME_RANGES.length] ?? TIME_RANGES[0]!);
1127
+ };
1128
+
1129
+ return [
1130
+ {
1131
+ id: "mode",
1132
+ key: "m",
1133
+ label: "ode",
1134
+ onPress: () => setViewState((current) => ({
1135
+ ...current,
1136
+ renderMode: current.renderMode === "line" ? "area" : "line",
1137
+ })),
1138
+ },
1139
+ {
1140
+ id: "resolution",
1141
+ key: "r",
1142
+ label: "es",
1143
+ onPress: () => {
1144
+ const currentIndex = resolutionChips.indexOf(effectiveResolution);
1145
+ const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
1146
+ setResolution(nextResolution);
1147
+ },
1148
+ },
1149
+ { id: "zoom", key: "+/-", label: "zoom", onPress: zoomIn },
1150
+ { id: "reset", key: "0", label: "reset", onPress: resetView },
1151
+ ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
1152
+ ];
1153
+ }, [effectiveResolution, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1154
+
1155
+ usePaneFooter("comparison-chart", () => ({
1156
+ order: 10,
1157
+ hints: footerHints,
1158
+ }), [footerHints]);
1088
1159
 
1089
- const result = effectiveRenderer === "kitty" ? staticResult : interactiveResult!;
1090
1160
  const liveScene = useMemo(() => (
1091
1161
  effectiveRenderer === "kitty"
1092
1162
  ? staticScene
@@ -1309,7 +1379,7 @@ function ComparisonStockChartView({
1309
1379
  rendererState.nativeReady,
1310
1380
  ]);
1311
1381
 
1312
- useKeyboard((event) => {
1382
+ useShortcut((event) => {
1313
1383
  if (!focused || symbols.length === 0) return;
1314
1384
 
1315
1385
  switch (event.name) {
@@ -1575,12 +1645,13 @@ function ComparisonStockChartView({
1575
1645
  };
1576
1646
 
1577
1647
  if (symbols.length === 0) {
1578
- return <text fg={colors.textDim}>No comparison tickers configured.</text>;
1648
+ return <Text fg={colors.textDim}>No comparison tickers configured.</Text>;
1579
1649
  }
1580
1650
 
1581
1651
  const hasChartData = series.some((entry) => entry.points.length > 0);
1582
- const selectedSeries = hasChartData ? (result.selectedSeries ?? staticResult.selectedSeries) : null;
1583
- const selectedPoint = hasChartData ? (result.selectedPoint ?? staticResult.selectedPoint) : null;
1652
+ const cursorScene = effectiveRenderer === "kitty" ? staticScene : liveScene;
1653
+ const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
1654
+ const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
1584
1655
  const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
1585
1656
  const selectedBaseValue = selectedSeries?.baseValue ?? null;
1586
1657
  const selectedChange = selectedRawValue !== null && selectedBaseValue !== null
@@ -1590,12 +1661,14 @@ function ComparisonStockChartView({
1590
1661
  ? (selectedChange / selectedBaseValue) * 100
1591
1662
  : null;
1592
1663
  const selectedCurrency = selectedSeries?.currency ?? "USD";
1593
- const displayDate = result.activeDate ?? staticResult.activeDate;
1664
+ const displayDate = cursorScene?.activeDate ?? staticResult.activeDate;
1594
1665
  const visiblePriceRange = result.priceRange ?? staticResult.priceRange ?? undefined;
1595
1666
  const axisLabels = new Map(staticResult.axisLabels.map((entry) => [entry.row, entry.label]));
1596
- const cursorAxisLabel = result.cursorRow !== null && result.crosshairValue !== null
1667
+ const cursorRow = useCanvasChart ? cursorScene?.cursorRow ?? null : result.cursorRow;
1668
+ const crosshairValue = useCanvasChart ? cursorScene?.crosshairValue ?? null : result.crosshairValue;
1669
+ const cursorAxisLabel = cursorRow !== null && crosshairValue !== null
1597
1670
  ? formatComparisonCursorAxisValue(
1598
- result.crosshairValue,
1671
+ crosshairValue,
1599
1672
  projection.effectiveAxisMode,
1600
1673
  visiblePriceRange,
1601
1674
  )
@@ -1605,57 +1678,188 @@ function ComparisonStockChartView({
1605
1678
  end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
1606
1679
  });
1607
1680
 
1681
+ const canvasBitmapSize = useMemo(() => {
1682
+ if (!canvasCharts) return null;
1683
+ const resolutionScale = Math.max(1, pixelRatio);
1684
+ return {
1685
+ pixelWidth: Math.max(1, Math.round(chartWidth * cellWidthPx * resolutionScale)),
1686
+ pixelHeight: Math.max(1, Math.round(chartHeight * cellHeightPx * resolutionScale)),
1687
+ };
1688
+ }, [canvasCharts, cellHeightPx, cellWidthPx, chartHeight, chartWidth, pixelRatio]);
1689
+
1690
+ const canvasProjection = useMemo(() => (
1691
+ canvasBitmapSize
1692
+ ? projectComparisonChartData(
1693
+ series,
1694
+ Math.max(chartWidth, canvasBitmapSize.pixelWidth),
1695
+ projectionViewState,
1696
+ axisMode,
1697
+ )
1698
+ : null
1699
+ ), [axisMode, canvasBitmapSize, chartWidth, projectionViewState, series]);
1700
+
1701
+ const canvasBaseScene = useMemo(() => (
1702
+ canvasProjection
1703
+ ? buildComparisonChartScene(canvasProjection, {
1704
+ width: Math.max(chartWidth, canvasBitmapSize?.pixelWidth ?? chartWidth),
1705
+ height: chartHeight,
1706
+ cursorX: null,
1707
+ cursorY: null,
1708
+ selectedSymbol: viewState.selectedSymbol,
1709
+ colors: chartColors,
1710
+ })
1711
+ : null
1712
+ ), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, viewState.selectedSymbol]);
1713
+
1714
+ const canvasBaseBitmapKey = useMemo(() => {
1715
+ if (!canvasBitmapSize || !canvasProjection || !hasChartData || isBlockingBody || bodyMessage) return null;
1716
+ return buildComparisonNativeBitmapKey(
1717
+ symbols.length,
1718
+ canvasProjection,
1719
+ viewState.selectedSymbol,
1720
+ canvasBitmapSize.pixelWidth,
1721
+ canvasBitmapSize.pixelHeight,
1722
+ [chartColors.bgColor, chartColors.gridColor, chartColors.crosshairColor].join(","),
1723
+ );
1724
+ }, [
1725
+ bodyMessage,
1726
+ canvasBitmapSize,
1727
+ canvasProjection,
1728
+ chartColors.bgColor,
1729
+ chartColors.crosshairColor,
1730
+ chartColors.gridColor,
1731
+ hasChartData,
1732
+ isBlockingBody,
1733
+ symbols.length,
1734
+ viewState.selectedSymbol,
1735
+ ]);
1736
+
1737
+ const canvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
1738
+ return canvasBaseBitmapKey && canvasBaseBitmapState?.key === canvasBaseBitmapKey
1739
+ ? canvasBaseBitmapState.bitmap
1740
+ : null;
1741
+ }, [canvasBaseBitmapKey, canvasBaseBitmapState]);
1742
+
1743
+ const visibleCanvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
1744
+ if (canvasBaseBitmap) return canvasBaseBitmap;
1745
+ if (!canvasBaseBitmapKey || !canvasBitmapSize || !canvasBaseBitmapState) return null;
1746
+ return canvasBaseBitmapState.bitmap.width === canvasBitmapSize.pixelWidth
1747
+ && canvasBaseBitmapState.bitmap.height === canvasBitmapSize.pixelHeight
1748
+ ? canvasBaseBitmapState.bitmap
1749
+ : null;
1750
+ }, [canvasBaseBitmap, canvasBaseBitmapKey, canvasBaseBitmapState, canvasBitmapSize]);
1751
+
1752
+ useEffect(() => {
1753
+ if (!canvasBitmapSize || !canvasBaseBitmapKey || !canvasBaseScene) {
1754
+ lastCanvasBaseBitmapRef.current = null;
1755
+ setCanvasBaseBitmapState((current) => (current === null ? current : null));
1756
+ return;
1757
+ }
1758
+
1759
+ const cachedBitmap = lastCanvasBaseBitmapRef.current?.key === canvasBaseBitmapKey
1760
+ ? lastCanvasBaseBitmapRef.current.bitmap
1761
+ : null;
1762
+ if (cachedBitmap) {
1763
+ setCanvasBaseBitmapState((current) => (
1764
+ current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap: cachedBitmap }
1765
+ ));
1766
+ return;
1767
+ }
1768
+
1769
+ let cancelled = false;
1770
+ const frame = requestAnimationFrameSafe(() => {
1771
+ const bitmap = renderNativeComparisonChartBase(canvasBaseScene, canvasBitmapSize.pixelWidth, canvasBitmapSize.pixelHeight);
1772
+ if (cancelled) return;
1773
+ lastCanvasBaseBitmapRef.current = { key: canvasBaseBitmapKey, bitmap };
1774
+ setCanvasBaseBitmapState((current) => (
1775
+ current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap }
1776
+ ));
1777
+ });
1778
+
1779
+ return () => {
1780
+ cancelled = true;
1781
+ cancelAnimationFrameSafe(frame);
1782
+ };
1783
+ }, [canvasBaseBitmapKey, canvasBaseScene, canvasBitmapSize]);
1784
+
1785
+ const canvasCrosshair = useMemo(() => {
1786
+ if (!canvasBitmapSize || !visibleCanvasBaseBitmap || !nativeCrosshair) return null;
1787
+ const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
1788
+ const overlayPixelX = scaleLocalPixelCoordinate(
1789
+ nativeCrosshair.pixelX,
1790
+ renderablePixelSize?.pixelWidth ?? canvasBitmapSize.pixelWidth,
1791
+ canvasBitmapSize.pixelWidth,
1792
+ );
1793
+ const overlayPixelY = scaleLocalPixelCoordinate(
1794
+ nativeCrosshair.pixelY,
1795
+ renderablePixelSize?.pixelHeight ?? canvasBitmapSize.pixelHeight,
1796
+ canvasBitmapSize.pixelHeight,
1797
+ );
1798
+ if (overlayPixelX === null || overlayPixelY === null) return null;
1799
+ return {
1800
+ pixelX: overlayPixelX,
1801
+ pixelY: overlayPixelY,
1802
+ color: nativeCrosshair.colors.crosshairColor,
1803
+ };
1804
+ }, [canvasBitmapSize, nativeCrosshair, renderer, visibleCanvasBaseBitmap]);
1805
+
1806
+ const plotBitmaps = useMemo(() => {
1807
+ if (!visibleCanvasBaseBitmap) return null;
1808
+ return [visibleCanvasBaseBitmap];
1809
+ }, [visibleCanvasBaseBitmap]);
1810
+
1608
1811
  const plotLines: Array<string | StyledContent> = effectiveRenderer === "kitty"
1609
1812
  ? blankPlotLines
1610
1813
  : result.lines;
1611
- const plotContent = plotLines.map((line, index) => (
1612
- <text key={index} content={line as unknown as string} />
1613
- ));
1814
+ const plotContent = plotBitmaps || (useCanvasChart && canvasBaseBitmapKey)
1815
+ ? null
1816
+ : plotLines.map((line, index) => (
1817
+ <Text key={index} content={line as unknown as string} />
1818
+ ));
1614
1819
 
1615
1820
  const plotBox = (
1616
- <box
1821
+ <ChartSurface
1617
1822
  ref={plotRef}
1618
1823
  width={chartWidth}
1619
1824
  height={chartHeight}
1620
1825
  flexDirection="column"
1621
- backgroundColor={chartColors.bgColor}
1826
+ bitmaps={plotBitmaps}
1827
+ crosshair={canvasCrosshair}
1622
1828
  onMouseMove={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotMove : undefined}
1623
1829
  onMouseDown={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotDown : undefined}
1624
1830
  onMouseUp={hasChartData && !isBlockingBody && !bodyMessage ? () => { dragRef.current = null; } : undefined}
1625
1831
  onMouseDrag={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotDrag : undefined}
1626
1832
  onMouseDragEnd={hasChartData && !isBlockingBody && !bodyMessage ? () => { dragRef.current = null; } : undefined}
1627
- onMouseOut={hasChartData && !isBlockingBody && !bodyMessage ? () => {
1628
- dragRef.current = null;
1629
- } : undefined}
1833
+ onMouseScroll={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotScroll : undefined}
1630
1834
  >
1631
1835
  {isBlockingBody
1632
1836
  ? (
1633
- <box flexGrow={1} alignItems="center" justifyContent="center">
1634
- <text fg={colors.textDim}>Loading chart...</text>
1635
- </box>
1837
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
1838
+ <Text fg={colors.textDim}>Loading chart...</Text>
1839
+ </Box>
1636
1840
  )
1637
1841
  : bodyMessage
1638
1842
  ? (
1639
- <box flexGrow={1} alignItems="center" justifyContent="center">
1640
- <text fg={colors.textDim}>{bodyMessage}</text>
1641
- </box>
1843
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
1844
+ <Text fg={colors.textDim}>{bodyMessage}</Text>
1845
+ </Box>
1642
1846
  )
1643
1847
  : plotContent}
1644
- </box>
1848
+ </ChartSurface>
1645
1849
  );
1646
1850
 
1647
1851
  const axisBox = (
1648
- <box width={axisSectionWidth} height={chartHeight} flexDirection="column">
1852
+ <Box width={axisSectionWidth} height={chartHeight} flexDirection="column">
1649
1853
  {Array.from({ length: chartHeight }, (_, row) => {
1650
- const isCursorRow = cursorAxisLabel !== null && result.cursorRow === row;
1854
+ const isCursorRow = cursorAxisLabel !== null && cursorRow === row;
1651
1855
  const label = isCursorRow ? cursorAxisLabel : (axisLabels.get(row) ?? null);
1652
1856
  return (
1653
- <text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
1857
+ <Text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
1654
1858
  {formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
1655
- </text>
1859
+ </Text>
1656
1860
  );
1657
1861
  })}
1658
- </box>
1862
+ </Box>
1659
1863
  );
1660
1864
 
1661
1865
  const legendRowsData = Array.from({ length: Math.ceil(symbols.length / legendColumns) }, (_, rowIndex) => (
@@ -1663,17 +1867,17 @@ function ComparisonStockChartView({
1663
1867
  ));
1664
1868
 
1665
1869
  return (
1666
- <box
1870
+ <Box
1667
1871
  flexDirection="column"
1668
1872
  flexGrow={1}
1669
1873
  onMouseScroll={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotScroll : undefined}
1670
1874
  >
1671
- <box flexDirection="row" gap={2} height={1}>
1672
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>
1875
+ <Box flexDirection="row" gap={2} height={1}>
1876
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
1673
1877
  {viewState.selectedSymbol ?? "Compare"} - {getChartResolutionLabel(effectiveResolution)}
1674
- </text>
1675
- <text fg={colors.textDim}>{visibleLabel}</text>
1676
- <text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1878
+ </Text>
1879
+ <Text fg={colors.textDim}>{visibleLabel}</Text>
1880
+ <Text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1677
1881
  {selectedRawValue !== null
1678
1882
  ? formatMarketPriceWithCurrency(selectedRawValue, selectedCurrency, {
1679
1883
  minimumFractionDigits: 2,
@@ -1681,8 +1885,8 @@ function ComparisonStockChartView({
1681
1885
  priceRange: visiblePriceRange,
1682
1886
  })
1683
1887
  : "—"}
1684
- </text>
1685
- <text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1888
+ </Text>
1889
+ <Text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1686
1890
  {selectedChange !== null && selectedChangePct !== null
1687
1891
  ? `${formatSignedMarketPrice(selectedChange, {
1688
1892
  minimumFractionDigits: 2,
@@ -1690,14 +1894,14 @@ function ComparisonStockChartView({
1690
1894
  priceRange: visiblePriceRange,
1691
1895
  })} (${formatPercentRaw(selectedChangePct)})`
1692
1896
  : "Waiting for data"}
1693
- </text>
1694
- {displayDate && <text fg={colors.textDim}>{formatDateShort(displayDate)}</text>}
1695
- </box>
1897
+ </Text>
1898
+ {displayDate && <Text fg={colors.textDim}>{formatDateShort(displayDate)}</Text>}
1899
+ </Box>
1696
1900
 
1697
- <box flexDirection="row" height={1}>
1698
- <box flexDirection="row" gap={1}>
1901
+ <Box flexDirection="row" height={1}>
1902
+ <Box flexDirection="row" gap={1}>
1699
1903
  {TIME_RANGES.map((range, index) => (
1700
- <text
1904
+ <Text
1701
1905
  key={range}
1702
1906
  fg={activePreset === range ? colors.textBright : (isRangePresetSupported(range, availableManualResolutions) ? colors.textDim : colors.textMuted)}
1703
1907
  attributes={activePreset === range ? TextAttributes.BOLD : 0}
@@ -1708,49 +1912,49 @@ function ComparisonStockChartView({
1708
1912
  }}
1709
1913
  >
1710
1914
  {`${index + 1}:${range}`}
1711
- </text>
1915
+ </Text>
1712
1916
  ))}
1713
- </box>
1714
- </box>
1917
+ </Box>
1918
+ </Box>
1715
1919
 
1716
- <box flexDirection="row" height={1}>
1717
- <box flexDirection="row" gap={1}>
1920
+ <Box flexDirection="row" height={1}>
1921
+ <Box flexDirection="row" gap={1}>
1718
1922
  {resolutionChips.map((resolution) => (
1719
- <text
1923
+ <Text
1720
1924
  key={resolution}
1721
1925
  fg={effectiveResolution === resolution ? colors.textBright : colors.textDim}
1722
1926
  attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
1723
1927
  onMouseDown={() => setResolution(resolution)}
1724
1928
  >
1725
1929
  {getChartResolutionLabel(resolution)}
1726
- </text>
1930
+ </Text>
1727
1931
  ))}
1728
1932
  {isUpdating && (
1729
- <text fg={colors.textDim}>updating</text>
1933
+ <Text fg={colors.textDim}>updating</Text>
1730
1934
  )}
1731
- </box>
1732
- <box flexGrow={1} />
1733
- <box flexDirection="row" gap={1}>
1935
+ </Box>
1936
+ <Box flexGrow={1} />
1937
+ <Box flexDirection="row" gap={1}>
1734
1938
  {COMPARISON_RENDER_MODES.map((mode) => (
1735
- <text
1939
+ <Text
1736
1940
  key={mode}
1737
1941
  fg={viewState.renderMode === mode ? colors.textBright : colors.textDim}
1738
1942
  attributes={viewState.renderMode === mode ? TextAttributes.BOLD : 0}
1739
1943
  onMouseDown={() => setViewState((current) => ({ ...current, renderMode: mode }))}
1740
1944
  >
1741
1945
  {MODE_CHIPS[mode]}
1742
- </text>
1946
+ </Text>
1743
1947
  ))}
1744
1948
  {projection.warning && (
1745
- <text fg={colors.textDim}>{projection.warning}</text>
1949
+ <Text fg={colors.textDim}>{projection.warning}</Text>
1746
1950
  )}
1747
- {rendererState.nativeUnavailable && (
1748
- <text fg={colors.textDim}>native unavailable</text>
1951
+ {showNativeUnavailable && (
1952
+ <Text fg={colors.textDim}>native unavailable</Text>
1749
1953
  )}
1750
- </box>
1751
- </box>
1954
+ </Box>
1955
+ </Box>
1752
1956
 
1753
- <box
1957
+ <Box
1754
1958
  flexDirection="row"
1755
1959
  height={chartHeight}
1756
1960
  gap={axisGap}
@@ -1758,17 +1962,17 @@ function ComparisonStockChartView({
1758
1962
  >
1759
1963
  {plotBox}
1760
1964
  {axisBox}
1761
- </box>
1965
+ </Box>
1762
1966
 
1763
- <box height={1}>
1764
- <text fg={colors.textDim}>{result.timeLabels || staticResult.timeLabels}</text>
1765
- </box>
1967
+ <Box height={timeAxisRows}>
1968
+ <Text fg={colors.textDim}>{timeAxisLabel}</Text>
1969
+ </Box>
1766
1970
 
1767
1971
  {legendRows > 0 && (
1768
- <scrollbox height={legendRows} scrollY>
1769
- <box flexDirection="column">
1972
+ <ScrollBox height={legendRows} scrollY>
1973
+ <Box flexDirection="column">
1770
1974
  {legendRowsData.map((legendRow, rowIndex) => (
1771
- <box key={`legend-row:${rowIndex}`} flexDirection="row" gap={1}>
1975
+ <Box key={`legend-row:${rowIndex}`} flexDirection="row" gap={1}>
1772
1976
  {legendRow.map((symbol) => {
1773
1977
  const item = projection.series.find((entry) => entry.symbol === symbol) ?? null;
1774
1978
  const isSelected = viewState.selectedSymbol === symbol;
@@ -1787,7 +1991,7 @@ function ComparisonStockChartView({
1787
1991
  : `${symbol} waiting`;
1788
1992
 
1789
1993
  return (
1790
- <box
1994
+ <Box
1791
1995
  key={symbol}
1792
1996
  width={legendItemWidth}
1793
1997
  backgroundColor={isSelected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
@@ -1797,59 +2001,51 @@ function ComparisonStockChartView({
1797
2001
  onOpenSymbol(symbol);
1798
2002
  }}
1799
2003
  >
1800
- <text fg={item?.color ?? colors.textDim} attributes={isSelected ? TextAttributes.BOLD : 0}>
2004
+ <Text fg={item?.color ?? colors.textDim} attributes={isSelected ? TextAttributes.BOLD : 0}>
1801
2005
  {clipText(`${isSelected ? ">" : " "} ${summary}`, legendItemWidth)}
1802
- </text>
1803
- </box>
2006
+ </Text>
2007
+ </Box>
1804
2008
  );
1805
2009
  })}
1806
- </box>
2010
+ </Box>
1807
2011
  ))}
1808
- </box>
1809
- </scrollbox>
2012
+ </Box>
2013
+ </ScrollBox>
1810
2014
  )}
1811
2015
 
1812
- <box height={1}>
1813
- <box flexDirection="row" gap={1}>
1814
- <ChartControlHint hotkey="m" label="ode" />
1815
- <ChartControlHint hotkey="r" label="es" />
1816
- <ChartControlHint hotkey="+/-" label="zoom" />
1817
- <ChartControlHint hotkey="0" label="reset" />
1818
- {width >= 72 && <ChartControlHint hotkey="1-7" label="range" />}
1819
- {width >= 88 && <ChartControlHint hotkey="up/down" label="legend" />}
1820
- </box>
1821
- </box>
1822
- </box>
2016
+ </Box>
1823
2017
  );
1824
2018
  }
1825
2019
 
1826
2020
  const MemoizedComparisonStockChartView = memo(ComparisonStockChartView);
1827
2021
 
1828
2022
  export function ComparisonStockChart(props: ComparisonStockChartProps) {
1829
- const { state } = useAppState();
2023
+ const tickers = useAppSelector((state) => state.tickers);
2024
+ const financials = useAppSelector((state) => state.financials);
2025
+ const chartPreferences = useAppSelector((state) => state.config.chartPreferences);
1830
2026
  const symbolsKey = props.symbols.join("|");
1831
2027
  const stableSymbols = useMemo(() => props.symbols, [symbolsKey]);
1832
2028
  const symbolSources = useMemo<ComparisonChartSymbolSource[]>(() => stableSymbols.map((symbol) => {
1833
- const ticker = state.tickers.get(symbol) ?? null;
1834
- const financials = state.financials.get(symbol) ?? null;
2029
+ const ticker = tickers.get(symbol) ?? null;
2030
+ const financial = financials.get(symbol) ?? null;
1835
2031
  const instrument = ticker?.metadata.broker_contracts?.[0] ?? null;
1836
2032
  return {
1837
2033
  symbol,
1838
- currency: financials?.quote?.currency ?? ticker?.metadata.currency,
2034
+ currency: financial?.quote?.currency ?? ticker?.metadata.currency,
1839
2035
  exchange: ticker?.metadata.exchange ?? "",
1840
2036
  brokerId: instrument?.brokerId,
1841
2037
  brokerInstanceId: instrument?.brokerInstanceId,
1842
2038
  instrument,
1843
- priceHistory: financials?.priceHistory ?? [],
2039
+ priceHistory: financial?.priceHistory ?? [],
1844
2040
  };
1845
- }), [stableSymbols, state.financials, state.tickers]);
2041
+ }), [financials, stableSymbols, tickers]);
1846
2042
 
1847
2043
  return (
1848
2044
  <MemoizedComparisonStockChartView
1849
2045
  {...props}
1850
2046
  symbols={stableSymbols}
1851
- defaultRenderMode={state.config.chartPreferences.defaultRenderMode}
1852
- preferredRenderer={state.config.chartPreferences.renderer}
2047
+ defaultRenderMode={chartPreferences.defaultRenderMode}
2048
+ preferredRenderer={chartPreferences.renderer}
1853
2049
  symbolSources={symbolSources}
1854
2050
  />
1855
2051
  );