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,36 +1,39 @@
1
+ import { Box } from "../../../ui";
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { useKeyboard } from "@opentui/react";
3
- import type { ScrollBoxRenderable } from "@opentui/core";
4
- import { TabBar } from "../../../components/tab-bar";
5
- import { getSharedRegistry } from "../../registry";
3
+ import {
4
+ Tabs,
5
+ usePaneFooter,
6
+ type DataTableKeyEvent,
7
+ type PaneFooterSegment,
8
+ type TickerListVisibleRange,
9
+ } from "../../../components";
10
+ import { createRowValueCache } from "../../../components/ui/row-value-cache";
11
+ import { usePluginTickerActions } from "../../plugin-runtime";
6
12
  import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
7
13
  import { useFxRatesMap, useTickerFinancialsMap } from "../../../market-data/hooks";
8
14
  import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
9
15
  import { useAppActive } from "../../../state/app-activity";
10
16
  import {
11
- useAppState,
17
+ useAppSelector,
12
18
  usePaneCollection,
13
19
  usePaneInstance,
14
- usePaneInstanceId,
15
20
  usePaneStateValue,
16
21
  type CollectionSortPreference,
17
22
  } from "../../../state/app-context";
18
- import { getCollectionTickers, getCollectionType } from "../../../state/selectors";
19
23
  import { useQuoteStreaming } from "../../../state/use-quote-streaming";
20
24
  import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
21
25
  import { getActiveQuoteDisplay } from "../../../utils/market-status";
22
- import type { ColumnConfig } from "../../../types/config";
26
+ import type { AppConfig, ColumnConfig } from "../../../types/config";
23
27
  import type { TickerRecord } from "../../../types/ticker";
24
28
  import type { PaneProps } from "../../../types/plugin";
25
29
  import type { TickerFinancials } from "../../../types/financials";
26
- import { getSortValue, resolveCollectionSortPreference, type ColumnContext } from "./metrics";
30
+ import { calculatePortfolioSummaryTotals, getSortValue, resolveCollectionSortPreference, type ColumnContext } from "./metrics";
27
31
  import {
28
32
  PortfolioCashMarginDrawer,
29
- PortfolioSummaryBar,
30
33
  shouldToggleCashMarginDrawer,
31
34
  usePortfolioAccountState,
32
35
  } from "./header";
33
- import type { ResolvedPortfolioAccountState } from "./summary";
36
+ import { buildPortfolioSummarySegments, type ResolvedPortfolioAccountState } from "./summary";
34
37
  import {
35
38
  getCollectionEntries,
36
39
  getPortfolioPaneSettings,
@@ -38,10 +41,16 @@ import {
38
41
  resolveScopedCollectionEntries,
39
42
  resolveVisibleColumns,
40
43
  } from "./settings";
44
+ import { formatPercentRaw } from "../../../utils/format";
45
+ import { getMostRecentQuoteUpdate } from "../../../utils/quote-time";
46
+ import { priceColor } from "../../../theme/colors";
41
47
  import { PortfolioTickerTable, type QuoteFlashDirection } from "./table";
42
48
  import { useThrottledCursorSymbol } from "./use-throttled-cursor-symbol";
43
49
 
44
50
  const VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS = 5 * 60_000;
51
+ const VISIBLE_FINANCIAL_WARMUP_DELAY_MS = 350;
52
+ const STREAM_OVERSCAN_ROWS = 6;
53
+ const sortValueCache = createRowValueCache<string, ReturnType<typeof getSortValue>>(5000);
45
54
 
46
55
  function needsVisibleFinancialWarmup(ticker: TickerRecord, financials: TickerFinancials | undefined): boolean {
47
56
  if (ticker.metadata.assetCategory === "OPT") return false;
@@ -50,11 +59,19 @@ function needsVisibleFinancialWarmup(ticker: TickerRecord, financials: TickerFin
50
59
  return financials.priceHistory.length === 0;
51
60
  }
52
61
 
53
- function selectStreamTickers(
62
+ export function selectStreamTickers(
54
63
  tickers: TickerRecord[],
55
- _visibleRange: { start: number; end: number },
64
+ visibleRange: { start: number; end: number },
65
+ selectedSymbol?: string | null,
56
66
  ) {
57
- return tickers;
67
+ const start = Math.max(0, visibleRange.start - STREAM_OVERSCAN_ROWS);
68
+ const end = Math.min(tickers.length, visibleRange.end + STREAM_OVERSCAN_ROWS);
69
+ const visible = tickers.slice(start, end);
70
+ if (!selectedSymbol || visible.some((ticker) => ticker.metadata.ticker === selectedSymbol)) {
71
+ return visible;
72
+ }
73
+ const selectedTicker = tickers.find((ticker) => ticker.metadata.ticker === selectedSymbol);
74
+ return selectedTicker ? [...visible, selectedTicker] : visible;
58
75
  }
59
76
 
60
77
  function buildTrackedCurrencies(
@@ -92,6 +109,31 @@ function buildTrackedCurrencies(
92
109
  return [...currencies];
93
110
  }
94
111
 
112
+ function getCollectionTypeFromConfig(config: AppConfig, collectionId: string | null): "portfolio" | "watchlist" | null {
113
+ if (!collectionId) return null;
114
+ if (config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
115
+ if (config.watchlists.some((watchlist) => watchlist.id === collectionId)) return "watchlist";
116
+ return null;
117
+ }
118
+
119
+ function getCollectionTickersFromConfig(
120
+ config: AppConfig,
121
+ tickersBySymbol: Map<string, TickerRecord>,
122
+ collectionId: string | null,
123
+ ): TickerRecord[] {
124
+ if (!collectionId) return [];
125
+ const isPortfolio = config.portfolios.some((portfolio) => portfolio.id === collectionId);
126
+ const isWatchlist = !isPortfolio && config.watchlists.some((watchlist) => watchlist.id === collectionId);
127
+ if (!isPortfolio && !isWatchlist) return [];
128
+ return [...tickersBySymbol.values()]
129
+ .filter((ticker) => (
130
+ isPortfolio
131
+ ? ticker.metadata.portfolios.includes(collectionId)
132
+ : ticker.metadata.watchlists.includes(collectionId)
133
+ ))
134
+ .sort((left, right) => left.metadata.ticker.localeCompare(right.metadata.ticker));
135
+ }
136
+
95
137
  function sortTickers(
96
138
  tickers: TickerRecord[],
97
139
  financialsMap: Map<string, TickerFinancials>,
@@ -103,10 +145,37 @@ function sortTickers(
103
145
 
104
146
  const sortColumn = columns.find((column) => column.id === sortPreference.columnId);
105
147
  if (!sortColumn) return tickers;
148
+ const exchangeRatesVersion = [...columnContext.exchangeRates]
149
+ .sort(([left], [right]) => left.localeCompare(right))
150
+ .map(([currency, rate]) => `${currency}:${rate}`)
151
+ .join(",");
152
+ const sortContextVersion = [
153
+ sortColumn.id,
154
+ columnContext.activeTab ?? "",
155
+ columnContext.baseCurrency,
156
+ exchangeRatesVersion,
157
+ sortColumn.id === "latency" ? columnContext.now : 0,
158
+ ].join("|");
159
+ const sortValues = new Map<string, ReturnType<typeof getSortValue>>();
160
+ for (const ticker of tickers) {
161
+ const financials = financialsMap.get(ticker.metadata.ticker);
162
+ const financialsVersion = [
163
+ financials?.quote?.lastUpdated ?? 0,
164
+ Object.keys(financials?.fundamentals ?? {}).length,
165
+ financials?.priceHistory.length ?? 0,
166
+ ].join(":");
167
+ const positionsVersion = JSON.stringify(ticker.metadata.positions);
168
+ const version = `${sortContextVersion}|${financialsVersion}|${positionsVersion}`;
169
+ sortValues.set(ticker.metadata.ticker, sortValueCache.get(
170
+ `${ticker.metadata.ticker}:${sortColumn.id}`,
171
+ version,
172
+ () => getSortValue(sortColumn, ticker, financials, columnContext),
173
+ ));
174
+ }
106
175
 
107
176
  return [...tickers].sort((leftTicker, rightTicker) => {
108
- const leftValue = getSortValue(sortColumn, leftTicker, financialsMap.get(leftTicker.metadata.ticker), columnContext);
109
- const rightValue = getSortValue(sortColumn, rightTicker, financialsMap.get(rightTicker.metadata.ticker), columnContext);
177
+ const leftValue = sortValues.get(leftTicker.metadata.ticker) ?? null;
178
+ const rightValue = sortValues.get(rightTicker.metadata.ticker) ?? null;
110
179
 
111
180
  if (leftValue == null && rightValue == null) return 0;
112
181
  if (leftValue == null) return 1;
@@ -121,11 +190,15 @@ function sortTickers(
121
190
  }
122
191
 
123
192
  export function PortfolioListPane({ focused, width, height }: PaneProps) {
124
- const registry = getSharedRegistry();
125
- const paneId = usePaneInstanceId();
193
+ const { pinTicker } = usePluginTickerActions();
126
194
  const paneInstance = usePaneInstance();
127
195
  const appActive = useAppActive();
128
- const { state } = useAppState();
196
+ const config = useAppSelector((state) => state.config);
197
+ const tickersBySymbol = useAppSelector((state) => state.tickers);
198
+ const cachedFinancials = useAppSelector((state) => state.financials);
199
+ const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
200
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
201
+ const refreshingSize = useAppSelector((state) => state.refreshing.size);
129
202
  const paneCollection = usePaneCollection();
130
203
 
131
204
  const [currentCollectionId, setCurrentCollectionId] = usePaneStateValue<string>("collectionId", paneCollection.collectionId ?? "");
@@ -133,7 +206,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
133
206
  const [collectionSorts, setCollectionSorts] = usePaneStateValue<Record<string, CollectionSortPreference>>("collectionSorts", {});
134
207
  const [cashDrawerExpanded, setCashDrawerExpanded] = usePaneStateValue<boolean>("cashDrawerExpanded", false);
135
208
 
136
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
137
209
  const [now, setNow] = useState(Date.now());
138
210
  const [flashSymbols, setFlashSymbols] = useState<Map<string, QuoteFlashDirection>>(new Map());
139
211
  const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
@@ -142,8 +214,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
142
214
  const mountedRef = useRef(true);
143
215
  const warmupInFlightRef = useRef(new Set<string>());
144
216
  const warmupAttemptRef = useRef(new Map<string, number>());
145
- const scrollRef = useRef<ScrollBoxRenderable>(null);
146
- const headerScrollRef = useRef<ScrollBoxRenderable>(null);
147
217
  const {
148
218
  cursorSymbol,
149
219
  setCursorSymbol,
@@ -156,54 +226,55 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
156
226
  [paneInstance?.settings],
157
227
  );
158
228
  const collectionEntries = useMemo(
159
- () => getCollectionEntries(state.config),
160
- [state.config],
229
+ () => getCollectionEntries(config),
230
+ [config],
161
231
  );
162
232
  const visibleCollections = useMemo(
163
233
  () => resolveScopedCollectionEntries(collectionEntries, paneSettings),
164
234
  [collectionEntries, paneSettings],
165
235
  );
166
- const activeCollectionId = resolveActiveCollectionId(currentCollectionId, visibleCollections, paneSettings);
167
- const isPortfolioTab = getCollectionType(state, activeCollectionId) === "portfolio";
236
+ const activeCollectionId = resolveActiveCollectionId(currentCollectionId, visibleCollections);
237
+ const isPortfolioTab = getCollectionTypeFromConfig(config, activeCollectionId) === "portfolio";
168
238
  const currentPortfolio = useMemo(() => (
169
239
  isPortfolioTab
170
- ? state.config.portfolios.find((portfolio) => portfolio.id === activeCollectionId) ?? null
240
+ ? config.portfolios.find((portfolio) => portfolio.id === activeCollectionId) ?? null
171
241
  : null
172
- ), [activeCollectionId, isPortfolioTab, state.config.portfolios]);
242
+ ), [activeCollectionId, config.portfolios, isPortfolioTab]);
173
243
 
174
244
  const tickers = useMemo(
175
- () => getCollectionTickers(state, activeCollectionId),
176
- [activeCollectionId, state.config.portfolios, state.config.watchlists, state.tickers],
245
+ () => getCollectionTickersFromConfig(config, tickersBySymbol, activeCollectionId),
246
+ [activeCollectionId, config, tickersBySymbol],
177
247
  );
178
248
  const marketFinancialsMap = useTickerFinancialsMap(tickers);
179
249
  const sharedCoordinator = getSharedMarketDataCoordinator();
180
250
  const financialsMap = useMemo(() => {
181
- const merged = sharedCoordinator ? new Map<string, TickerFinancials>() : new Map(state.financials);
251
+ const merged = sharedCoordinator ? new Map<string, TickerFinancials>() : new Map(cachedFinancials);
182
252
  for (const [symbol, financials] of marketFinancialsMap) {
183
253
  merged.set(symbol, financials);
184
254
  }
185
255
  return merged;
186
- }, [marketFinancialsMap, sharedCoordinator, state.financials]);
256
+ }, [cachedFinancials, marketFinancialsMap, sharedCoordinator]);
187
257
 
188
- const accountState = usePortfolioAccountState(currentPortfolio, state);
258
+ const accountStateInput = useMemo(() => ({ brokerAccounts, config }), [brokerAccounts, config]);
259
+ const accountState = usePortfolioAccountState(currentPortfolio, accountStateInput);
189
260
  const columns = useMemo(
190
261
  () => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab),
191
262
  [isPortfolioTab, paneSettings.columnIds],
192
263
  );
193
264
 
194
265
  const trackedCurrencies = useMemo(
195
- () => buildTrackedCurrencies(tickers, financialsMap, accountState, state.config.baseCurrency),
196
- [tickers, financialsMap, accountState, state.config.baseCurrency],
266
+ () => buildTrackedCurrencies(tickers, financialsMap, accountState, config.baseCurrency),
267
+ [accountState, config.baseCurrency, financialsMap, tickers],
197
268
  );
198
269
  const fetchedExchangeRates = useFxRatesMap(trackedCurrencies);
199
- const effectiveExchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, state.exchangeRates);
270
+ const effectiveExchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, cachedExchangeRates);
200
271
 
201
272
  const columnContext: ColumnContext = useMemo(() => ({
202
273
  activeTab: isPortfolioTab ? activeCollectionId : undefined,
203
- baseCurrency: state.config.baseCurrency,
274
+ baseCurrency: config.baseCurrency,
204
275
  exchangeRates: effectiveExchangeRates,
205
276
  now,
206
- }), [activeCollectionId, effectiveExchangeRates, isPortfolioTab, now, state.config.baseCurrency]);
277
+ }), [activeCollectionId, config.baseCurrency, effectiveExchangeRates, isPortfolioTab, now]);
207
278
 
208
279
  const activeSort = resolveCollectionSortPreference(activeCollectionId, isPortfolioTab, collectionSorts);
209
280
  const sortedTickers = useMemo(
@@ -219,37 +290,17 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
219
290
  const requestedDrawerHeight = showCashDrawer
220
291
  ? (cashDrawerExpanded ? Math.min(6, Math.max(3, 2 + accountState.visibleCashBalances.length)) : 1)
221
292
  : 0;
222
- const headerHeight = paneSettings.hideHeader
223
- ? (paneSettings.hideTabs ? 0 : 1)
224
- : paneSettings.hideTabs ? 1 : 2;
293
+ const showCollectionTabs = visibleCollections.length > 1;
294
+ const headerHeight = showCollectionTabs ? 1 : 0;
225
295
  const drawerHeight = showCashDrawer
226
296
  ? Math.min(requestedDrawerHeight, Math.max(1, height - (headerHeight + 2)))
227
297
  : 0;
228
298
 
229
- const syncHeaderScroll = useCallback(() => {
230
- const bodyScrollBox = scrollRef.current;
231
- const headerScrollBox = headerScrollRef.current;
232
- if (bodyScrollBox && headerScrollBox && headerScrollBox.scrollLeft !== bodyScrollBox.scrollLeft) {
233
- headerScrollBox.scrollLeft = bodyScrollBox.scrollLeft;
234
- }
235
- }, []);
236
-
237
- const updateStreamWindow = useCallback(() => {
238
- const scrollBox = scrollRef.current;
239
- if (!scrollBox) return;
240
-
241
- const buffer = 3;
242
- const start = Math.max(0, scrollBox.scrollTop - buffer);
243
- const end = Math.min(sortedTickers.length, scrollBox.scrollTop + scrollBox.viewport.height + buffer);
299
+ const handleVisibleRangeChange = useCallback(({ start, end }: TickerListVisibleRange) => {
244
300
  setStreamWindow((current) => (
245
301
  current.start === start && current.end === end ? current : { start, end }
246
302
  ));
247
- }, [sortedTickers.length]);
248
-
249
- const handleBodyScrollActivity = useCallback(() => {
250
- syncHeaderScroll();
251
- updateStreamWindow();
252
- }, [syncHeaderScroll, updateStreamWindow]);
303
+ }, []);
253
304
 
254
305
  const handleCollectionSelect = useCallback((collectionId: string) => {
255
306
  cancelPendingCursorSymbol();
@@ -276,77 +327,69 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
276
327
  setSortPreference({ columnId, direction: "asc" });
277
328
  }, [activeSort.columnId, activeSort.direction, setSortPreference]);
278
329
 
279
- const handleKeyboard = useCallback((event: { name?: string; shift?: boolean }) => {
330
+ const openTickerFloating = useCallback((symbol: string) => {
331
+ pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
332
+ }, [pinTicker]);
333
+
334
+ const handleRowActivate = useCallback((ticker: TickerRecord) => {
335
+ flushCursorSymbol(ticker.metadata.ticker);
336
+ openTickerFloating(ticker.metadata.ticker);
337
+ }, [flushCursorSymbol, openTickerFloating]);
338
+
339
+ const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
280
340
  if (!focused) return;
281
341
 
282
342
  const key = event.name;
283
343
  const isEnter = key === "enter" || key === "return";
284
344
 
285
345
  if (isEnter && event.shift) {
346
+ event.preventDefault?.();
347
+ event.stopPropagation?.();
286
348
  const ticker = sortedTickers[safeSelectedIdx];
287
349
  if (ticker) {
288
- registry?.pinTickerFn(ticker.metadata.ticker, { floating: true, paneType: "ticker-detail" });
350
+ openTickerFloating(ticker.metadata.ticker);
289
351
  }
290
- return;
352
+ return true;
291
353
  }
292
354
 
293
355
  if (shouldToggleCashMarginDrawer(key, showCashDrawer)) {
356
+ event.preventDefault?.();
357
+ event.stopPropagation?.();
294
358
  setCashDrawerExpanded(!cashDrawerExpanded);
295
- return;
359
+ return true;
296
360
  }
297
361
 
298
- if (key === "j" || key === "down") {
299
- const nextTicker = sortedTickers[Math.min(safeSelectedIdx + 1, sortedTickers.length - 1)];
300
- if (nextTicker) setCursorSymbol(nextTicker.metadata.ticker);
301
- return;
302
- }
303
-
304
- if (key === "k" || key === "up") {
305
- const nextTicker = sortedTickers[Math.max(safeSelectedIdx - 1, 0)];
306
- if (nextTicker) setCursorSymbol(nextTicker.metadata.ticker);
307
- return;
308
- }
309
-
310
- if (!paneSettings.hideTabs && (key === "h" || key === "left")) {
362
+ if (showCollectionTabs && (key === "h" || key === "left")) {
363
+ event.preventDefault?.();
364
+ event.stopPropagation?.();
311
365
  const previousCollection = visibleCollections[Math.max(currentTabIdx - 1, 0)];
312
366
  if (previousCollection) handleCollectionSelect(previousCollection.id);
313
- return;
367
+ return true;
314
368
  }
315
369
 
316
- if (!paneSettings.hideTabs && (key === "l" || key === "right")) {
370
+ if (showCollectionTabs && (key === "l" || key === "right")) {
371
+ event.preventDefault?.();
372
+ event.stopPropagation?.();
317
373
  const nextCollection = visibleCollections[Math.min(currentTabIdx + 1, visibleCollections.length - 1)];
318
374
  if (nextCollection) handleCollectionSelect(nextCollection.id);
319
- return;
375
+ return true;
320
376
  }
321
377
 
322
- if (!isEnter) return;
323
-
324
- flushCursorSymbol(cursorSymbol);
325
-
326
- const ticker = sortedTickers[safeSelectedIdx];
327
- if (ticker) {
328
- registry?.navigateTickerFn(ticker.metadata.ticker);
329
- }
378
+ return false;
330
379
  }, [
331
380
  cashDrawerExpanded,
332
381
  currentTabIdx,
333
382
  focused,
334
- paneId,
335
- paneSettings.hideTabs,
336
- registry,
383
+ openTickerFloating,
337
384
  safeSelectedIdx,
338
385
  setCashDrawerExpanded,
339
- setCursorSymbol,
340
- flushCursorSymbol,
341
386
  handleCollectionSelect,
342
387
  showCashDrawer,
388
+ showCollectionTabs,
343
389
  sortedTickers,
344
- state.config.layout.instances,
345
390
  visibleCollections,
346
391
  ]);
347
392
 
348
- useKeyboard(handleKeyboard);
349
-
350
393
  useEffect(() => {
351
394
  if (activeCollectionId !== currentCollectionId) {
352
395
  cancelPendingCursorSymbol();
@@ -360,33 +403,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
360
403
  };
361
404
  }, []);
362
405
 
363
- useEffect(() => {
364
- if (headerScrollRef.current) {
365
- headerScrollRef.current.horizontalScrollBar.visible = false;
366
- }
367
- syncHeaderScroll();
368
- }, [syncHeaderScroll]);
369
-
370
- useEffect(() => {
371
- const scrollBox = scrollRef.current;
372
- if (!scrollBox) return;
373
-
374
- const viewportHeight = scrollBox.viewport.height;
375
- if (safeSelectedIdx < scrollBox.scrollTop) {
376
- scrollBox.scrollTo(safeSelectedIdx);
377
- } else if (safeSelectedIdx >= scrollBox.scrollTop + viewportHeight) {
378
- scrollBox.scrollTo(safeSelectedIdx - viewportHeight + 1);
379
- }
380
- queueMicrotask(updateStreamWindow);
381
- }, [safeSelectedIdx, updateStreamWindow]);
382
-
383
- useEffect(() => {
384
- const scrollBox = scrollRef.current;
385
- if (!scrollBox) return;
386
- scrollBox.verticalScrollBar.visible = sortedTickers.length > scrollBox.viewport.height;
387
- updateStreamWindow();
388
- }, [sortedTickers.length, drawerHeight, cashDrawerExpanded, updateStreamWindow]);
389
-
390
406
  useEffect(() => {
391
407
  if (!appActive) return;
392
408
  const timerId = setInterval(() => setNow(Date.now()), 1000);
@@ -427,8 +443,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
427
443
  }, [cursorSymbol, setCursorSymbol, sortedTickers]);
428
444
 
429
445
  const streamTickers = useMemo(
430
- () => selectStreamTickers(sortedTickers, streamWindow),
431
- [sortedTickers, streamWindow],
446
+ () => selectStreamTickers(sortedTickers, streamWindow, cursorSymbol),
447
+ [cursorSymbol, sortedTickers, streamWindow],
432
448
  );
433
449
  const streamTargets = useMemo(() => (
434
450
  streamTickers
@@ -442,6 +458,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
442
458
 
443
459
  useEffect(() => {
444
460
  if (!appActive) return;
461
+ if (!focused) return;
445
462
  if (!sharedCoordinator) return;
446
463
 
447
464
  const nowTimestamp = Date.now();
@@ -453,7 +470,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
453
470
  });
454
471
  if (queue.length === 0) return;
455
472
 
473
+ let cancelled = false;
456
474
  const runNext = async (): Promise<void> => {
475
+ if (cancelled) return;
457
476
  const nextTicker = queue.shift();
458
477
  if (!nextTicker) return;
459
478
 
@@ -471,70 +490,133 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
471
490
  warmupInFlightRef.current.delete(key);
472
491
  }
473
492
 
474
- if (mountedRef.current) {
493
+ if (mountedRef.current && !cancelled) {
475
494
  await runNext();
476
495
  }
477
496
  };
478
497
 
479
- const workers = Array.from({ length: Math.min(3, queue.length) }, () => runNext());
480
- void Promise.all(workers);
481
- }, [appActive, financialsMap, sharedCoordinator, visibleFinancialTickers]);
498
+ const timeoutId = setTimeout(() => {
499
+ void runNext();
500
+ }, VISIBLE_FINANCIAL_WARMUP_DELAY_MS);
501
+
502
+ return () => {
503
+ cancelled = true;
504
+ clearTimeout(timeoutId);
505
+ };
506
+ }, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers]);
482
507
 
483
508
  useQuoteStreaming(streamTargets);
484
509
 
510
+ const summaryFooterInfo = useMemo<PaneFooterSegment[]>(() => {
511
+ if (paneSettings.hideHeader) return [];
512
+
513
+ const lastRefreshTimestamp = getMostRecentQuoteUpdate(
514
+ sortedTickers.map((ticker) => financialsMap.get(ticker.metadata.ticker)?.quote),
515
+ );
516
+ const refreshText = refreshingSize > 0
517
+ ? "Refreshing..."
518
+ : lastRefreshTimestamp != null
519
+ ? new Date(lastRefreshTimestamp).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
520
+ : "-";
521
+ const totals = calculatePortfolioSummaryTotals(
522
+ sortedTickers,
523
+ financialsMap,
524
+ config.baseCurrency,
525
+ effectiveExchangeRates,
526
+ isPortfolioTab,
527
+ activeCollectionId,
528
+ );
529
+
530
+ if (!isPortfolioTab) {
531
+ if (totals.watchlistCount === 0) return [];
532
+ return [
533
+ {
534
+ id: "avg-day",
535
+ parts: [
536
+ { text: "Avg Day", tone: "label" },
537
+ { text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: priceColor(totals.avgWatchlistChange), bold: true },
538
+ ],
539
+ },
540
+ {
541
+ id: "refresh",
542
+ parts: [{ text: refreshText, tone: "muted" }],
543
+ },
544
+ ];
545
+ }
546
+
547
+ if (!totals.hasPositions && !accountState) return [];
548
+ return buildPortfolioSummarySegments({
549
+ totals,
550
+ accountState: accountState ? { account: accountState.account, sourceLabel: accountState.sourceLabel } : null,
551
+ widthBudget: Math.max(16, width - 14),
552
+ refreshText,
553
+ }).map((segment) => ({
554
+ id: segment.id,
555
+ parts: segment.parts,
556
+ }));
557
+ }, [
558
+ accountState,
559
+ activeCollectionId,
560
+ effectiveExchangeRates,
561
+ financialsMap,
562
+ isPortfolioTab,
563
+ paneSettings.hideHeader,
564
+ config.baseCurrency,
565
+ refreshingSize,
566
+ sortedTickers,
567
+ width,
568
+ ]);
569
+
570
+ usePaneFooter("portfolio-list", () => ({
571
+ info: summaryFooterInfo,
572
+ hints: showCashDrawer
573
+ ? [{
574
+ id: "cash",
575
+ key: "c",
576
+ label: "ash",
577
+ onPress: () => setCashDrawerExpanded(!cashDrawerExpanded),
578
+ }]
579
+ : [],
580
+ }), [cashDrawerExpanded, setCashDrawerExpanded, showCashDrawer, summaryFooterInfo]);
581
+
485
582
  return (
486
- <box flexDirection="column" flexGrow={1}>
487
- <box flexDirection="column" height={headerHeight}>
488
- {!paneSettings.hideTabs && (
489
- <box flexDirection="row" height={1}>
490
- <box flexShrink={1} overflow="hidden">
491
- <TabBar
583
+ <Box flexDirection="column" width={width} height={height}>
584
+ <Box flexDirection="column" height={headerHeight}>
585
+ {showCollectionTabs && (
586
+ <Box flexDirection="row" height={1}>
587
+ <Box flexShrink={1} overflow="hidden">
588
+ <Tabs
492
589
  tabs={visibleCollections.map((collection) => ({ label: collection.name, value: collection.id }))}
493
590
  activeValue={activeCollectionId}
494
591
  onSelect={handleCollectionSelect}
495
592
  compact
496
593
  />
497
- </box>
498
- </box>
499
- )}
500
- {!paneSettings.hideHeader && (
501
- <box height={1}>
502
- <PortfolioSummaryBar
503
- tickers={sortedTickers}
504
- financialsMap={financialsMap}
505
- baseCurrency={state.config.baseCurrency}
506
- exchangeRates={effectiveExchangeRates}
507
- refreshingCount={state.refreshing.size}
508
- isPortfolio={isPortfolioTab}
509
- collectionId={activeCollectionId}
510
- width={Math.max(0, width)}
511
- accountState={accountState}
512
- />
513
- </box>
594
+ </Box>
595
+ </Box>
514
596
  )}
515
- </box>
597
+ </Box>
516
598
 
517
599
  <PortfolioTickerTable
518
600
  columns={columns}
601
+ focused={focused}
519
602
  sortColumnId={activeSort.columnId}
520
603
  sortDirection={activeSort.direction}
521
604
  onHeaderClick={handleHeaderClick}
522
- headerScrollRef={headerScrollRef}
523
- scrollRef={scrollRef}
524
- syncHeaderScroll={syncHeaderScroll}
525
- onBodyScrollActivity={handleBodyScrollActivity}
526
605
  sortedTickers={sortedTickers}
527
606
  cursorSymbol={cursorSymbol}
528
- hoveredIdx={hoveredIdx}
529
- setHoveredIdx={setHoveredIdx}
530
607
  setCursorSymbol={setCursorSymbol}
531
608
  financialsMap={financialsMap}
532
609
  columnContext={columnContext}
533
610
  flashSymbols={flashSymbols}
611
+ onRootKeyDown={handleTableKeyDown}
612
+ onVisibleRangeChange={handleVisibleRangeChange}
613
+ visibleRangeBuffer={3}
614
+ resetScrollKey={activeCollectionId}
615
+ onRowActivate={handleRowActivate}
534
616
  />
535
617
 
536
618
  {showCashDrawer && accountState && (
537
- <box height={drawerHeight} paddingX={1}>
619
+ <Box height={drawerHeight} paddingX={1}>
538
620
  <PortfolioCashMarginDrawer
539
621
  accountState={accountState}
540
622
  expanded={cashDrawerExpanded}
@@ -542,8 +624,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
542
624
  width={Math.max(0, width - 2)}
543
625
  height={drawerHeight}
544
626
  />
545
- </box>
627
+ </Box>
546
628
  )}
547
- </box>
629
+ </Box>
548
630
  );
549
631
  }