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
@@ -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,254 @@
1
+ import { useImperativeHandle, useLayoutEffect, useRef, type ForwardedRef, type RefObject } from "react";
2
+ import { useShortcut } from "../react/input";
3
+ import { useNativeRenderer, type ScrollBoxRenderable } from "../ui/host";
4
+ import { useAppSelector, useOptionalPaneInstanceId } from "./app-context";
5
+
6
+ type ScrollDirection = "up" | "down";
7
+
8
+ interface PaneScrollAction {
9
+ direction: ScrollDirection;
10
+ resolveTarget(scrollBox: ScrollBoxRenderable): number | null;
11
+ }
12
+
13
+ interface PaneScrollEntry {
14
+ order: number;
15
+ ref: RefObject<ScrollBoxRenderable | null>;
16
+ onScrollActivity?: (event: { scroll: { direction: ScrollDirection; delta: number } }) => void;
17
+ }
18
+
19
+ interface PaneScrollKeyEvent {
20
+ name?: string;
21
+ ctrl?: boolean;
22
+ meta?: boolean;
23
+ alt?: boolean;
24
+ option?: boolean;
25
+ defaultPrevented?: boolean;
26
+ propagationStopped?: boolean;
27
+ preventDefault?: () => void;
28
+ stopPropagation?: () => void;
29
+ }
30
+
31
+ const paneScrollBoxes = new Map<string, Map<number, PaneScrollEntry>>();
32
+ let nextScrollEntryId = 1;
33
+ let nextScrollEntryOrder = 1;
34
+ const MIN_ARROW_SCROLL_LINES = 3;
35
+
36
+ function hasHiddenAncestor(scrollBox: ScrollBoxRenderable): boolean {
37
+ let node: unknown = scrollBox;
38
+ const seen = new Set<unknown>();
39
+
40
+ while (node && typeof node === "object" && !seen.has(node)) {
41
+ seen.add(node);
42
+ const renderable = node as { visible?: unknown; parent?: unknown };
43
+ if (renderable.visible === false) return true;
44
+ node = renderable.parent;
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ function hasCollapsedBounds(scrollBox: ScrollBoxRenderable): boolean {
51
+ const bounds = scrollBox.getBoundingClientRect?.();
52
+ return bounds != null && (bounds.width <= 0 || bounds.height <= 0);
53
+ }
54
+
55
+ function isVisibleScrollBox(scrollBox: ScrollBoxRenderable): boolean {
56
+ return !hasHiddenAncestor(scrollBox) && !hasCollapsedBounds(scrollBox);
57
+ }
58
+
59
+ function clampScrollTop(scrollBox: ScrollBoxRenderable, target: number): number | null {
60
+ const viewportHeight = Math.max(0, scrollBox.viewport?.height ?? 0);
61
+ if (viewportHeight <= 0) return null;
62
+ const maxScrollTop = Math.max(0, scrollBox.scrollHeight - viewportHeight);
63
+ if (maxScrollTop <= 0) return null;
64
+ return Math.max(0, Math.min(maxScrollTop, target));
65
+ }
66
+
67
+ function scrollTargetByDelta(scrollBox: ScrollBoxRenderable, delta: number): number | null {
68
+ const target = clampScrollTop(scrollBox, scrollBox.scrollTop + delta);
69
+ return target == null || target === scrollBox.scrollTop ? null : target;
70
+ }
71
+
72
+ function arrowScrollLines(scrollBox: ScrollBoxRenderable): number {
73
+ return Math.max(MIN_ARROW_SCROLL_LINES, Math.floor((scrollBox.viewport?.height ?? 0) / 4));
74
+ }
75
+
76
+ function resolveKeyScrollAction(event: PaneScrollKeyEvent): PaneScrollAction | null {
77
+ if (event.ctrl || event.meta || event.alt || event.option) return null;
78
+
79
+ switch (event.name) {
80
+ case "down":
81
+ return {
82
+ direction: "down",
83
+ resolveTarget: (scrollBox) => scrollTargetByDelta(scrollBox, arrowScrollLines(scrollBox)),
84
+ };
85
+ case "up":
86
+ return {
87
+ direction: "up",
88
+ resolveTarget: (scrollBox) => scrollTargetByDelta(scrollBox, -arrowScrollLines(scrollBox)),
89
+ };
90
+ case "pagedown":
91
+ return {
92
+ direction: "down",
93
+ resolveTarget: (scrollBox) => scrollTargetByDelta(
94
+ scrollBox,
95
+ Math.max(1, (scrollBox.viewport?.height ?? 1) - 1),
96
+ ),
97
+ };
98
+ case "pageup":
99
+ return {
100
+ direction: "up",
101
+ resolveTarget: (scrollBox) => scrollTargetByDelta(
102
+ scrollBox,
103
+ -Math.max(1, (scrollBox.viewport?.height ?? 1) - 1),
104
+ ),
105
+ };
106
+ case "home":
107
+ return {
108
+ direction: "up",
109
+ resolveTarget: (scrollBox) => (
110
+ scrollBox.scrollTop > 0 ? 0 : null
111
+ ),
112
+ };
113
+ case "end":
114
+ return {
115
+ direction: "down",
116
+ resolveTarget: (scrollBox) => {
117
+ const target = clampScrollTop(scrollBox, Number.MAX_SAFE_INTEGER);
118
+ return target == null || target === scrollBox.scrollTop ? null : target;
119
+ },
120
+ };
121
+ default:
122
+ return null;
123
+ }
124
+ }
125
+
126
+ function getEntryScore(entry: PaneScrollEntry, scrollBox: ScrollBoxRenderable): number {
127
+ const viewportHeight = scrollBox.viewport?.height ?? 0;
128
+ const scrollHeight = scrollBox.scrollHeight;
129
+ return viewportHeight * 100_000 + Math.min(scrollHeight, 99_999) + entry.order / 100_000;
130
+ }
131
+
132
+ function findScrollableEntry(paneId: string, action: PaneScrollAction): {
133
+ entry: PaneScrollEntry;
134
+ scrollBox: ScrollBoxRenderable;
135
+ target: number;
136
+ } | null {
137
+ const entries = paneScrollBoxes.get(paneId);
138
+ if (!entries) return null;
139
+
140
+ let best: { entry: PaneScrollEntry; scrollBox: ScrollBoxRenderable; target: number; score: number } | null = null;
141
+ for (const entry of entries.values()) {
142
+ const scrollBox = entry.ref.current;
143
+ if (!scrollBox) continue;
144
+ if (!isVisibleScrollBox(scrollBox)) continue;
145
+
146
+ const target = action.resolveTarget(scrollBox);
147
+ if (target == null) continue;
148
+
149
+ const score = getEntryScore(entry, scrollBox);
150
+ if (!best || score > best.score) {
151
+ best = { entry, scrollBox, target, score };
152
+ }
153
+ }
154
+
155
+ if (!best) return null;
156
+ return { entry: best.entry, scrollBox: best.scrollBox, target: best.target };
157
+ }
158
+
159
+ function scrollPaneByKey(paneId: string, event: PaneScrollKeyEvent): boolean {
160
+ const action = resolveKeyScrollAction(event);
161
+ if (!action) return false;
162
+
163
+ const match = findScrollableEntry(paneId, action);
164
+ if (!match) return false;
165
+
166
+ const previousTop = match.scrollBox.scrollTop;
167
+ match.scrollBox.scrollTo(match.target);
168
+ if (match.scrollBox.scrollTop !== match.target) {
169
+ match.scrollBox.scrollTop = match.target;
170
+ }
171
+
172
+ const delta = Math.abs(match.scrollBox.scrollTop - previousTop);
173
+ if (delta > 0) {
174
+ match.entry.onScrollActivity?.({ scroll: { direction: action.direction, delta } });
175
+ }
176
+ return delta > 0;
177
+ }
178
+
179
+ export function useForwardedScrollBoxRef<T>(
180
+ forwardedRef: ForwardedRef<T>,
181
+ ): RefObject<T | null> {
182
+ const localRef = useRef<T | null>(null);
183
+ useImperativeHandle(forwardedRef, () => localRef.current as T);
184
+ return localRef;
185
+ }
186
+
187
+ export function useRegisterPaneScrollBox(
188
+ ref: RefObject<ScrollBoxRenderable | null>,
189
+ options: {
190
+ enabled: boolean;
191
+ onScrollActivity?: PaneScrollEntry["onScrollActivity"];
192
+ },
193
+ ): void {
194
+ const paneId = useOptionalPaneInstanceId();
195
+ const entryIdRef = useRef<number | null>(null);
196
+
197
+ useLayoutEffect(() => {
198
+ if (!paneId || !options.enabled) return;
199
+
200
+ const entryId = nextScrollEntryId++;
201
+ entryIdRef.current = entryId;
202
+ let entries = paneScrollBoxes.get(paneId);
203
+ if (!entries) {
204
+ entries = new Map();
205
+ paneScrollBoxes.set(paneId, entries);
206
+ }
207
+ entries.set(entryId, {
208
+ order: nextScrollEntryOrder++,
209
+ ref,
210
+ onScrollActivity: options.onScrollActivity,
211
+ });
212
+
213
+ return () => {
214
+ const currentEntries = paneScrollBoxes.get(paneId);
215
+ currentEntries?.delete(entryId);
216
+ if (currentEntries?.size === 0) {
217
+ paneScrollBoxes.delete(paneId);
218
+ }
219
+ if (entryIdRef.current === entryId) {
220
+ entryIdRef.current = null;
221
+ }
222
+ };
223
+ }, [paneId, options.enabled, options.onScrollActivity, ref]);
224
+ }
225
+
226
+ export function PaneKeyboardScrollController({
227
+ paneId,
228
+ focused,
229
+ }: {
230
+ paneId: string;
231
+ focused: boolean;
232
+ }) {
233
+ const nativeRenderer = useNativeRenderer();
234
+ const inputCaptured = useAppSelector((state) => state.inputCaptured);
235
+ const focusedRef = useRef(focused);
236
+ const inputCapturedRef = useRef(inputCaptured);
237
+ const paneIdRef = useRef(paneId);
238
+
239
+ focusedRef.current = focused;
240
+ inputCapturedRef.current = inputCaptured;
241
+ paneIdRef.current = paneId;
242
+
243
+ useShortcut((event) => {
244
+ if (!focusedRef.current || inputCapturedRef.current) return;
245
+ if (event.defaultPrevented || event.propagationStopped) return;
246
+ if (!scrollPaneByKey(paneIdRef.current, event)) return;
247
+
248
+ event.preventDefault();
249
+ event.stopPropagation();
250
+ nativeRenderer.requestRender();
251
+ }, { phase: "after" });
252
+
253
+ return null;
254
+ }
@@ -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();