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,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
  }