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
@@ -0,0 +1,35 @@
1
+ import { saveConfig } from "../data/config-store";
2
+ import type { AppConfig } from "../types/config";
3
+ import { debugLog } from "../utils/debug-log";
4
+ import { measurePerfAsync } from "../utils/perf-marks";
5
+ import {
6
+ CONFIG_SAVE_DEBOUNCE_MS,
7
+ createPersistScheduler,
8
+ } from "./persist-scheduler";
9
+
10
+ const log = debugLog.createLogger("persist");
11
+
12
+ const configSaveScheduler = createPersistScheduler<AppConfig>({
13
+ delayMs: CONFIG_SAVE_DEBOUNCE_MS,
14
+ save: (config) => measurePerfAsync("persist.config.save", () => saveConfig(config)),
15
+ onError: (error) => {
16
+ log.warn("config.save.failed", { error: error instanceof Error ? error.message : String(error) });
17
+ },
18
+ });
19
+
20
+ export function scheduleConfigSave(config: AppConfig): void {
21
+ configSaveScheduler.schedule(config);
22
+ }
23
+
24
+ export function flushScheduledConfigSave(): Promise<void> {
25
+ return configSaveScheduler.flush();
26
+ }
27
+
28
+ export function cancelScheduledConfigSave(): void {
29
+ configSaveScheduler.cancel();
30
+ }
31
+
32
+ export async function saveConfigImmediately(config: AppConfig): Promise<void> {
33
+ cancelScheduledConfigSave();
34
+ await measurePerfAsync("persist.config.save", () => saveConfig(config));
35
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createPersistScheduler } from "./persist-scheduler";
3
+
4
+ function delay(ms: number): Promise<void> {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+
8
+ describe("createPersistScheduler", () => {
9
+ test("coalesces scheduled saves and writes the latest value", async () => {
10
+ const saved: number[] = [];
11
+ const scheduler = createPersistScheduler<number>({
12
+ delayMs: 5,
13
+ save: (value) => saved.push(value),
14
+ });
15
+
16
+ scheduler.schedule(1);
17
+ scheduler.schedule(2);
18
+ scheduler.schedule(3);
19
+ await delay(15);
20
+
21
+ expect(saved).toEqual([3]);
22
+ });
23
+
24
+ test("flush saves immediately", async () => {
25
+ const saved: string[] = [];
26
+ const scheduler = createPersistScheduler<string>({
27
+ delayMs: 1000,
28
+ save: (value) => saved.push(value),
29
+ });
30
+
31
+ scheduler.schedule("pending");
32
+ await scheduler.flush();
33
+
34
+ expect(saved).toEqual(["pending"]);
35
+ });
36
+
37
+ test("cancel drops pending value", async () => {
38
+ const saved: string[] = [];
39
+ const scheduler = createPersistScheduler<string>({
40
+ delayMs: 5,
41
+ save: (value) => saved.push(value),
42
+ });
43
+
44
+ scheduler.schedule("pending");
45
+ scheduler.cancel();
46
+ await delay(15);
47
+
48
+ expect(saved).toEqual([]);
49
+ });
50
+
51
+ test("save errors are reported without escaping timer", async () => {
52
+ const errors: unknown[] = [];
53
+ const scheduler = createPersistScheduler<string>({
54
+ delayMs: 5,
55
+ save: () => {
56
+ throw new Error("boom");
57
+ },
58
+ onError: (error) => errors.push(error),
59
+ });
60
+
61
+ scheduler.schedule("pending");
62
+ await delay(15);
63
+
64
+ expect(errors).toHaveLength(1);
65
+ expect(errors[0]).toBeInstanceOf(Error);
66
+ });
67
+ });
@@ -0,0 +1,69 @@
1
+ export const CONFIG_SAVE_DEBOUNCE_MS = 500;
2
+ export const SESSION_SAVE_DEBOUNCE_MS = 1000;
3
+ export const PLUGIN_STATE_SAVE_DEBOUNCE_MS = 500;
4
+
5
+ export interface PersistSchedulerOptions<T> {
6
+ delayMs: number;
7
+ save: (value: T) => Promise<void> | void;
8
+ onError?: (error: unknown) => void;
9
+ }
10
+
11
+ export interface PersistScheduler<T> {
12
+ schedule(value: T): void;
13
+ flush(): Promise<void>;
14
+ cancel(): void;
15
+ }
16
+
17
+ export function createPersistScheduler<T>({
18
+ delayMs,
19
+ save,
20
+ onError,
21
+ }: PersistSchedulerOptions<T>): PersistScheduler<T> {
22
+ let timer: ReturnType<typeof setTimeout> | null = null;
23
+ let pendingValue: T | undefined;
24
+ let hasPendingValue = false;
25
+ let inFlight: Promise<void> = Promise.resolve();
26
+
27
+ const clearTimer = () => {
28
+ if (!timer) return;
29
+ clearTimeout(timer);
30
+ timer = null;
31
+ };
32
+
33
+ const runSave = async (value: T) => {
34
+ try {
35
+ await save(value);
36
+ } catch (error) {
37
+ onError?.(error);
38
+ }
39
+ };
40
+
41
+ const drain = async () => {
42
+ clearTimer();
43
+ if (!hasPendingValue) return inFlight;
44
+ const value = pendingValue as T;
45
+ pendingValue = undefined;
46
+ hasPendingValue = false;
47
+ inFlight = inFlight.then(() => runSave(value));
48
+ return inFlight;
49
+ };
50
+
51
+ return {
52
+ schedule(value: T): void {
53
+ pendingValue = value;
54
+ hasPendingValue = true;
55
+ clearTimer();
56
+ timer = setTimeout(() => {
57
+ void drain();
58
+ }, Math.max(0, delayMs));
59
+ },
60
+ flush(): Promise<void> {
61
+ return drain();
62
+ },
63
+ cancel(): void {
64
+ clearTimer();
65
+ pendingValue = undefined;
66
+ hasPendingValue = false;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,55 @@
1
+ import type { AppState } from "../core/state/app-state";
2
+ import type { LayoutConfig, SavedLayout } from "../types/config";
3
+ import type { ReleaseInfo, UpdateProgress } from "../updater";
4
+
5
+ export function selectLayout(state: AppState): LayoutConfig {
6
+ return state.config.layout;
7
+ }
8
+
9
+ export function selectFocusedPaneId(state: AppState): string | null {
10
+ return state.focusedPaneId;
11
+ }
12
+
13
+ export function selectCommandBarOpen(state: AppState): boolean {
14
+ return state.commandBarOpen;
15
+ }
16
+
17
+ export function selectStatusBarVisible(state: AppState): boolean {
18
+ return state.statusBarVisible;
19
+ }
20
+
21
+ export function selectBaseCurrency(state: AppState): string {
22
+ return state.config.baseCurrency;
23
+ }
24
+
25
+ export function selectSavedLayouts(state: AppState): SavedLayout[] {
26
+ return state.config.layouts;
27
+ }
28
+
29
+ export function selectActiveLayoutIndex(state: AppState): number {
30
+ return state.config.activeLayoutIndex;
31
+ }
32
+
33
+ export function selectGridlockTipVisible(state: AppState): boolean {
34
+ return state.gridlockTipVisible;
35
+ }
36
+
37
+ export function selectGridlockTipSequence(state: AppState): number {
38
+ return state.gridlockTipSequence;
39
+ }
40
+
41
+ export function selectUpdateAvailable(state: AppState): ReleaseInfo | null {
42
+ return state.updateAvailable;
43
+ }
44
+
45
+ export function selectUpdateProgress(state: AppState): UpdateProgress | null {
46
+ return state.updateProgress;
47
+ }
48
+
49
+ export function selectUpdateCheckInProgress(state: AppState): boolean {
50
+ return state.updateCheckInProgress;
51
+ }
52
+
53
+ export function selectUpdateNotice(state: AppState): string | null {
54
+ return state.updateNotice;
55
+ }
@@ -1,63 +1,29 @@
1
1
  import type { AppState } from "./app-context";
2
- import { resolveCollectionForPane } from "./app-context";
3
2
  import type { TickerRecord } from "../types/ticker";
4
3
 
5
- /** Get tickers belonging to a specific portfolio */
6
- export function getPortfolioTickers(state: AppState, portfolioId: string): TickerRecord[] {
7
- const result: TickerRecord[] = [];
8
- for (const ticker of state.tickers.values()) {
9
- if (ticker.metadata.portfolios.includes(portfolioId)) {
10
- result.push(ticker);
11
- }
12
- }
13
- return result.sort((a, b) => a.metadata.ticker.localeCompare(b.metadata.ticker));
14
- }
15
-
16
- /** Get tickers belonging to a specific watchlist */
17
- export function getWatchlistTickers(state: AppState, watchlistId: string): TickerRecord[] {
18
- const result: TickerRecord[] = [];
19
- for (const ticker of state.tickers.values()) {
20
- if (ticker.metadata.watchlists.includes(watchlistId)) {
21
- result.push(ticker);
22
- }
23
- }
24
- return result.sort((a, b) => a.metadata.ticker.localeCompare(b.metadata.ticker));
4
+ function getCollectionMembershipKey(state: AppState, collectionId: string | null): "portfolios" | "watchlists" | null {
5
+ if (!collectionId) return null;
6
+ if (state.config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolios";
7
+ if (state.config.watchlists.some((watchlist) => watchlist.id === collectionId)) return "watchlists";
8
+ return null;
25
9
  }
26
10
 
27
11
  export function getCollectionTickers(state: AppState, collectionId: string | null): TickerRecord[] {
28
- if (!collectionId) return [];
29
- if (state.config.portfolios.some((portfolio) => portfolio.id === collectionId)) {
30
- return getPortfolioTickers(state, collectionId);
31
- }
32
- if (state.config.watchlists.some((watchlist) => watchlist.id === collectionId)) {
33
- return getWatchlistTickers(state, collectionId);
34
- }
35
- return [];
12
+ const membershipKey = getCollectionMembershipKey(state, collectionId);
13
+ if (!collectionId || !membershipKey) return [];
14
+ return [...state.tickers.values()]
15
+ .filter((ticker) => ticker.metadata[membershipKey].includes(collectionId))
16
+ .sort((a, b) => a.metadata.ticker.localeCompare(b.metadata.ticker));
36
17
  }
37
18
 
38
19
  export function getCollectionTickerCount(state: AppState, collectionId: string | null): number {
39
- if (!collectionId) return 0;
40
-
20
+ const membershipKey = getCollectionMembershipKey(state, collectionId);
21
+ if (!collectionId || !membershipKey) return 0;
41
22
  let count = 0;
42
- if (state.config.portfolios.some((portfolio) => portfolio.id === collectionId)) {
43
- for (const ticker of state.tickers.values()) {
44
- if (ticker.metadata.portfolios.includes(collectionId)) {
45
- count += 1;
46
- }
47
- }
48
- return count;
49
- }
50
-
51
- if (state.config.watchlists.some((watchlist) => watchlist.id === collectionId)) {
52
- for (const ticker of state.tickers.values()) {
53
- if (ticker.metadata.watchlists.includes(collectionId)) {
54
- count += 1;
55
- }
56
- }
57
- return count;
23
+ for (const ticker of state.tickers.values()) {
24
+ if (ticker.metadata[membershipKey].includes(collectionId)) count += 1;
58
25
  }
59
-
60
- return 0;
26
+ return count;
61
27
  }
62
28
 
63
29
  export function getCollectionName(state: AppState, collectionId: string | null): string {
@@ -68,25 +34,3 @@ export function getCollectionName(state: AppState, collectionId: string | null):
68
34
  if (watchlist) return watchlist.name;
69
35
  return collectionId;
70
36
  }
71
-
72
- export function getCollectionType(state: AppState, collectionId: string | null): "portfolio" | "watchlist" | null {
73
- if (!collectionId) return null;
74
- if (state.config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
75
- if (state.config.watchlists.some((watchlist) => watchlist.id === collectionId)) return "watchlist";
76
- return null;
77
- }
78
-
79
- export function getPaneCollectionTickers(state: AppState, paneId: string): TickerRecord[] {
80
- return getCollectionTickers(state, resolveCollectionForPane(state, paneId));
81
- }
82
-
83
- export function getAllCollections(state: AppState): Array<{ id: string; name: string; type: "portfolio" | "watchlist" }> {
84
- const collections: Array<{ id: string; name: string; type: "portfolio" | "watchlist" }> = [];
85
- for (const portfolio of state.config.portfolios) {
86
- collections.push({ id: portfolio.id, name: portfolio.name, type: "portfolio" });
87
- }
88
- for (const watchlist of state.config.watchlists) {
89
- collections.push({ id: watchlist.id, name: watchlist.name, type: "watchlist" });
90
- }
91
- return collections;
92
- }
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import { createDefaultConfig, createPaneInstance } from "../types/config";
3
3
  import type { TickerFinancials } from "../types/financials";
4
4
  import type { TickerRecord } from "../types/ticker";
5
- import { buildAppSessionSnapshot, reconcileAppSessionSnapshot } from "./session-persistence";
5
+ import { buildAppSessionSnapshot, reconcileAppSessionSnapshot } from "../core/state/session-persistence";
6
6
 
7
7
  function createTicker(symbol: string, exchange = "NASDAQ"): TickerRecord {
8
8
  return {
@@ -1,14 +1,26 @@
1
+
2
+ import { debugLog } from "../utils/debug-log";
3
+ import { measurePerfAsync } from "../utils/perf-marks";
4
+
1
5
  export interface TickerRefreshTask {
2
6
  key: string;
3
7
  priority: number;
4
8
  run: () => Promise<void>;
5
9
  }
6
10
 
11
+ interface QueuedTickerRefreshTask extends TickerRefreshTask {
12
+ enqueuedAt: number;
13
+ }
14
+
15
+ const TASK_WARN_MS = 500;
16
+ const WAIT_WARN_MS = 1000;
17
+ const refreshQueueLog = debugLog.createLogger("refresh-queue");
18
+
7
19
  export class TickerRefreshQueue {
8
20
  private active = 0;
9
21
  private paused = false;
10
22
  private readonly pendingKeys = new Set<string>();
11
- private readonly queue: TickerRefreshTask[] = [];
23
+ private readonly queue: QueuedTickerRefreshTask[] = [];
12
24
 
13
25
  constructor(private readonly concurrency = 3) {}
14
26
 
@@ -20,8 +32,15 @@ export class TickerRefreshQueue {
20
32
  enqueue(task: TickerRefreshTask): void {
21
33
  if (this.pendingKeys.has(task.key)) return;
22
34
  this.pendingKeys.add(task.key);
23
- this.queue.push(task);
35
+ this.queue.push({ ...task, enqueuedAt: Date.now() });
24
36
  this.queue.sort((a, b) => a.priority - b.priority);
37
+ refreshQueueLog.info("task queued", {
38
+ key: task.key,
39
+ priority: task.priority,
40
+ active: this.active,
41
+ pending: this.queue.length,
42
+ paused: this.paused,
43
+ });
25
44
  this.pump();
26
45
  }
27
46
 
@@ -30,7 +49,37 @@ export class TickerRefreshQueue {
30
49
  while (this.active < this.concurrency && this.queue.length > 0) {
31
50
  const next = this.queue.shift()!;
32
51
  this.active += 1;
33
- void next.run().finally(() => {
52
+ const waitMs = Date.now() - next.enqueuedAt;
53
+ refreshQueueLog.info("task started", {
54
+ key: next.key,
55
+ priority: next.priority,
56
+ waitMs,
57
+ active: this.active,
58
+ pending: this.queue.length,
59
+ });
60
+ void measurePerfAsync(
61
+ `refresh-queue.${next.key}`,
62
+ next.run,
63
+ {
64
+ key: next.key,
65
+ priority: next.priority,
66
+ waitMs,
67
+ },
68
+ ).finally(() => {
69
+ const durationMs = Date.now() - next.enqueuedAt - waitMs;
70
+ const payload = {
71
+ key: next.key,
72
+ priority: next.priority,
73
+ waitMs,
74
+ durationMs,
75
+ active: this.active - 1,
76
+ pending: this.queue.length,
77
+ };
78
+ if (durationMs >= TASK_WARN_MS || waitMs >= WAIT_WARN_MS) {
79
+ refreshQueueLog.warn("task finished slowly", payload);
80
+ } else {
81
+ refreshQueueLog.info("task finished", payload);
82
+ }
34
83
  this.active -= 1;
35
84
  this.pendingKeys.delete(next.key);
36
85
  this.pump();
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { getSharedRegistry } from "../plugins/registry";
3
- import { useAppState } from "./app-context";
3
+ import { useAppDispatch, useAppSelector } from "./app-context";
4
4
  import { resolveTickerSearch, upsertTickerFromSearchResult } from "../utils/ticker-search";
5
5
  import { collectUniqueTickerSymbols } from "../utils/ticker-tokenizer";
6
6
  import { useQuoteStreaming } from "./use-quote-streaming";
@@ -28,15 +28,17 @@ export function useInlineTickers(texts: readonly string[]): {
28
28
  catalog: Record<string, InlineTickerCatalogEntry>;
29
29
  openTicker: (symbol: string) => void;
30
30
  } {
31
- const { state, dispatch } = useAppState();
31
+ const dispatch = useAppDispatch();
32
+ const tickers = useAppSelector((state) => state.tickers);
33
+ const financials = useAppSelector((state) => state.financials);
32
34
  const registry = getSharedRegistry();
33
35
  const [, setRefreshVersion] = useState(0);
34
36
  const textsKey = texts.join("\u0000");
35
37
  const symbols = useMemo(() => normalizeSymbols(texts), [textsKey]);
36
38
  const symbolsKey = symbols.join("|");
37
- const latestRef = useRef({ state, dispatch, registry });
39
+ const latestRef = useRef({ tickers, financials, dispatch, registry });
38
40
 
39
- latestRef.current = { state, dispatch, registry };
41
+ latestRef.current = { tickers, financials, dispatch, registry };
40
42
 
41
43
  useEffect(() => {
42
44
  let mounted = true;
@@ -46,9 +48,9 @@ export function useInlineTickers(texts: readonly string[]): {
46
48
  };
47
49
 
48
50
  for (const symbol of symbols) {
49
- const currentState = latestRef.current.state;
50
- const currentTicker = currentState.tickers.get(symbol) ?? null;
51
- const currentQuote = currentState.financials.get(symbol)?.quote ?? null;
51
+ const current = latestRef.current;
52
+ const currentTicker = current.tickers.get(symbol) ?? null;
53
+ const currentQuote = current.financials.get(symbol)?.quote ?? null;
52
54
 
53
55
  if (currentTicker && currentQuote) {
54
56
  missingSymbols.delete(symbol);
@@ -66,8 +68,8 @@ export function useInlineTickers(texts: readonly string[]): {
66
68
  const resolved = await resolveTickerSearch({
67
69
  query: symbol,
68
70
  activeTicker: null,
69
- tickers: current.state.tickers,
70
- dataProvider: activeRegistry.dataProvider,
71
+ tickers: current.tickers,
72
+ dataProvider: activeRegistry.marketData,
71
73
  });
72
74
 
73
75
  if (!resolved) {
@@ -106,11 +108,11 @@ export function useInlineTickers(texts: readonly string[]): {
106
108
  const activeRegistry = current.registry;
107
109
  if (!activeRegistry) return;
108
110
 
109
- const latestTicker = current.state.tickers.get(symbol);
111
+ const latestTicker = current.tickers.get(symbol);
110
112
  if (!latestTicker) return;
111
113
 
112
114
  const instrument = latestTicker.metadata.broker_contracts?.[0] ?? null;
113
- const quote = await activeRegistry.dataProvider.getQuote(
115
+ const quote = await activeRegistry.marketData.getQuote(
114
116
  symbol,
115
117
  latestTicker.metadata.exchange,
116
118
  instrument
@@ -134,10 +136,10 @@ export function useInlineTickers(texts: readonly string[]): {
134
136
  return () => {
135
137
  mounted = false;
136
138
  };
137
- }, [dispatch, registry, state.financials, state.tickers, symbols, symbolsKey]);
139
+ }, [dispatch, financials, registry, symbols, symbolsKey, tickers]);
138
140
 
139
141
  const streamingTargets = symbols
140
- .map((symbol) => state.tickers.get(symbol))
142
+ .map((symbol) => tickers.get(symbol))
141
143
  .filter((ticker): ticker is TickerRecord => ticker != null)
142
144
  .map((ticker) => {
143
145
  const instrument = ticker.metadata.broker_contracts?.[0] ?? null;
@@ -157,13 +159,13 @@ export function useInlineTickers(texts: readonly string[]): {
157
159
  useQuoteStreaming(streamingTargets);
158
160
 
159
161
  const openTicker = useCallback((symbol: string) => {
160
- registry?.pinTickerFn(symbol, { floating: true, paneType: "ticker-detail" });
162
+ registry?.pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
161
163
  }, [registry]);
162
164
 
163
165
  const catalog: Record<string, InlineTickerCatalogEntry> = {};
164
166
  for (const symbol of symbols) {
165
- const ticker = state.tickers.get(symbol) ?? null;
166
- const quote = state.financials.get(symbol)?.quote ?? null;
167
+ const ticker = tickers.get(symbol) ?? null;
168
+ const quote = financials.get(symbol)?.quote ?? null;
167
169
  const status: InlineTickerStatus = ticker
168
170
  ? (quote ? "ready" : "loading")
169
171
  : (missingSymbols.has(symbol) ? "missing" : "loading");
@@ -28,10 +28,4 @@ describe("buildQuoteStreamSubscriptionKey", () => {
28
28
  expect(workKey).not.toBe(personalKey);
29
29
  });
30
30
 
31
- test("uses the same key for equivalent plain provider targets", () => {
32
- const first = buildQuoteStreamSubscriptionKey({ symbol: "MSFT", exchange: "NASDAQ" });
33
- const second = buildQuoteStreamSubscriptionKey({ symbol: "MSFT", exchange: "NASDAQ" });
34
-
35
- expect(first).toBe(second);
36
- });
37
31
  });
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useState } from "react";
3
- import { testRender } from "@opentui/react/test-utils";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
4
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../market-data/coordinator";
5
5
  import { useQuoteStreaming } from "./use-quote-streaming";
6
6
 
@@ -2,17 +2,18 @@ import { useEffect } from "react";
2
2
  import { useAppActive } from "./app-activity";
3
3
  import type { QuoteSubscriptionTarget } from "../types/data-provider";
4
4
  import { debugLog } from "../utils/debug-log";
5
+ import { canonicalExchange, normalizeSymbol } from "../utils/exchanges";
5
6
  import { getSharedMarketDataCoordinator } from "../market-data/coordinator";
6
7
 
7
8
  const quoteStreamLog = debugLog.createLogger("quote-stream");
8
9
 
9
10
  function normalizeTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
10
- const symbol = target.symbol.trim().toUpperCase();
11
+ const symbol = normalizeSymbol(target.symbol);
11
12
  if (!symbol) return null;
12
13
  return {
13
14
  ...target,
14
15
  symbol,
15
- exchange: target.exchange?.trim().toUpperCase() ?? "",
16
+ exchange: canonicalExchange(target.exchange),
16
17
  };
17
18
  }
18
19
 
@@ -3,8 +3,13 @@ import type { SessionStore } from "../data/session-store";
3
3
  import {
4
4
  buildAppSessionSnapshot,
5
5
  type AppSessionSnapshot,
6
- } from "./session-persistence";
6
+ } from "../core/state/session-persistence";
7
7
  import type { AppState } from "./app-context";
8
+ import { measurePerf } from "../utils/perf-marks";
9
+ import {
10
+ createPersistScheduler,
11
+ SESSION_SAVE_DEBOUNCE_MS,
12
+ } from "./persist-scheduler";
8
13
 
9
14
  export function usePersistSessionSnapshot(
10
15
  sessionStore: SessionStore | undefined,
@@ -25,20 +30,33 @@ export function usePersistSessionSnapshot(
25
30
  schemaVersion,
26
31
  };
27
32
 
28
- const saveSnapshotRef = useRef<() => void>(() => {});
29
- saveSnapshotRef.current = () => {
33
+ const schedulerRef = useRef<ReturnType<typeof createPersistScheduler<AppSessionSnapshot>> | null>(null);
34
+ if (!schedulerRef.current) {
35
+ schedulerRef.current = createPersistScheduler<AppSessionSnapshot>({
36
+ delayMs: SESSION_SAVE_DEBOUNCE_MS,
37
+ save: (snapshot) => {
38
+ const {
39
+ sessionStore: currentStore,
40
+ sessionId: currentSessionId,
41
+ schemaVersion: currentSchemaVersion,
42
+ } = latestRef.current;
43
+ if (!currentStore) return;
44
+ measurePerf("persist.session.save", () => {
45
+ currentStore.set(currentSessionId, snapshot, currentSchemaVersion);
46
+ }, { sessionId: currentSessionId });
47
+ },
48
+ });
49
+ }
50
+
51
+ const buildSnapshot = (): AppSessionSnapshot | null => {
30
52
  const {
31
- sessionStore: currentStore,
32
53
  state: currentState,
33
- sessionId: currentSessionId,
34
- schemaVersion: currentSchemaVersion,
35
54
  } = latestRef.current;
36
55
 
37
- if (!currentStore) return;
38
- if (!currentState.initialized && currentState.tickers.size === 0) return;
56
+ if (!currentState.initialized && currentState.tickers.size === 0) return null;
39
57
 
40
58
  try {
41
- currentStore.set(currentSessionId, buildAppSessionSnapshot({
59
+ return buildAppSessionSnapshot({
42
60
  config: currentState.config,
43
61
  paneState: currentState.paneState,
44
62
  focusedPaneId: currentState.focusedPaneId,
@@ -47,21 +65,18 @@ export function usePersistSessionSnapshot(
47
65
  recentTickers: currentState.recentTickers,
48
66
  tickers: currentState.tickers,
49
67
  exchangeRates: currentState.exchangeRates,
50
- }) satisfies AppSessionSnapshot, currentSchemaVersion);
68
+ }) satisfies AppSessionSnapshot;
51
69
  } catch {
52
70
  // Snapshot persistence is best-effort during teardown.
71
+ return null;
53
72
  }
54
73
  };
55
74
 
56
75
  useEffect(() => {
57
76
  if (!sessionStore) return;
58
77
  if (!state.initialized && state.tickers.size === 0) return;
59
- const timer = setTimeout(() => {
60
- saveSnapshotRef.current();
61
- }, 250);
62
- return () => {
63
- clearTimeout(timer);
64
- };
78
+ const snapshot = buildSnapshot();
79
+ if (snapshot) schedulerRef.current?.schedule(snapshot);
65
80
  }, [
66
81
  sessionStore,
67
82
  sessionId,
@@ -78,7 +93,7 @@ export function usePersistSessionSnapshot(
78
93
 
79
94
  useEffect(() => {
80
95
  return () => {
81
- saveSnapshotRef.current();
96
+ void schedulerRef.current?.flush();
82
97
  };
83
98
  }, []);
84
99
  }