gloomberb 0.5.1 → 0.6.1

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