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,118 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readdir } from "fs/promises";
3
+ import { join, relative } from "path";
4
+
5
+ const SOURCE_ROOT = join(process.cwd(), "src");
6
+ const RUNTIME_EXTENSIONS = new Set([".ts", ".tsx"]);
7
+ const IMPORT_PATTERN = /\b(?:import|export)\s+(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|import\(["']([^"']+)["']\)/g;
8
+ const OPENTUI_JSX_PATTERN = /<\s*\/?\s*(box|text|scrollbox|input|textarea|span|strong|u)(?=[\s>/])/g;
9
+
10
+ function isRuntimeSource(path: string): boolean {
11
+ if (path.includes(".test.")) return false;
12
+ if (path.includes("test-helpers.")) return false;
13
+ if (path.includes("__snapshots__")) return false;
14
+ for (const extension of RUNTIME_EXTENSIONS) {
15
+ if (path.endsWith(extension)) return true;
16
+ }
17
+ return false;
18
+ }
19
+
20
+ async function collectSourceFiles(dir: string, result: string[] = []): Promise<string[]> {
21
+ const entries = await readdir(dir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ const path = join(dir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ await collectSourceFiles(path, result);
26
+ continue;
27
+ }
28
+ if (entry.isFile() && isRuntimeSource(path)) {
29
+ result.push(path);
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ async function collectImports(): Promise<Array<{ file: string; specifier: string }>> {
36
+ const files = await collectSourceFiles(SOURCE_ROOT);
37
+ const imports: Array<{ file: string; specifier: string }> = [];
38
+ for (const file of files) {
39
+ const source = await Bun.file(file).text();
40
+ for (const match of source.matchAll(IMPORT_PATTERN)) {
41
+ imports.push({
42
+ file: relative(process.cwd(), file),
43
+ specifier: match[1] ?? match[2] ?? "",
44
+ });
45
+ }
46
+ }
47
+ return imports;
48
+ }
49
+
50
+ describe("import boundaries", () => {
51
+ test("external renderer packages stay in renderer adapters", async () => {
52
+ const imports = await collectImports();
53
+ const violations = imports.filter(({ file, specifier }) => {
54
+ if (specifier.startsWith("@opentui/") || specifier.startsWith("@opentui-ui/")) {
55
+ return !file.startsWith("src/renderers/opentui/");
56
+ }
57
+ if (specifier.startsWith("#opentui/")) {
58
+ return true;
59
+ }
60
+ if (specifier.startsWith("electrobun/")) {
61
+ return !file.startsWith("src/renderers/electrobun/");
62
+ }
63
+ if (specifier === "react-dom" || specifier.startsWith("react-dom/")) {
64
+ return !file.startsWith("src/renderers/electrobun/");
65
+ }
66
+ return false;
67
+ });
68
+
69
+ expect(violations).toEqual([]);
70
+ });
71
+
72
+ test("core and shared react layers do not import renderer packages directly", async () => {
73
+ const imports = await collectImports();
74
+ const violations = imports.filter(({ file, specifier }) => {
75
+ if (!file.startsWith("src/core/") && !file.startsWith("src/react/")) return false;
76
+ return (
77
+ specifier.startsWith("@opentui/")
78
+ || specifier.startsWith("@opentui-ui/")
79
+ || specifier.startsWith("#opentui/")
80
+ || specifier.startsWith("electrobun/")
81
+ || specifier === "react-dom"
82
+ || specifier.startsWith("react-dom/")
83
+ );
84
+ });
85
+
86
+ expect(violations).toEqual([]);
87
+ });
88
+
89
+ test("core state stays out of React app context", async () => {
90
+ const imports = await collectImports();
91
+ const violations = imports.filter(({ file, specifier }) => {
92
+ if (!file.startsWith("src/core/")) return false;
93
+ return specifier === "react"
94
+ || specifier.startsWith("react/")
95
+ || specifier.includes("/state/app-context")
96
+ || specifier.endsWith("/state/app-context");
97
+ });
98
+
99
+ expect(violations).toEqual([]);
100
+ });
101
+
102
+ test("shared runtime code uses Gloom UI components instead of OpenTUI intrinsic tags", async () => {
103
+ const files = await collectSourceFiles(SOURCE_ROOT);
104
+ const violations: Array<{ file: string; tag: string }> = [];
105
+
106
+ for (const file of files) {
107
+ const relativeFile = relative(process.cwd(), file);
108
+ if (relativeFile.startsWith("src/renderers/")) continue;
109
+
110
+ const source = await Bun.file(file).text();
111
+ for (const match of source.matchAll(OPENTUI_JSX_PATTERN)) {
112
+ violations.push({ file: relativeFile, tag: match[1] ?? "" });
113
+ }
114
+ }
115
+
116
+ expect(violations).toEqual([]);
117
+ });
118
+ });
Binary file
@@ -0,0 +1,73 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
2
+ <title id="title">Gloomberb candlestick logo</title>
3
+ <desc id="desc">Three rounded candlesticks on a dark square background.</desc>
4
+ <defs>
5
+ <linearGradient id="greenBody" x1="143.5" y1="173" x2="206.5" y2="323" gradientUnits="userSpaceOnUse">
6
+ <stop offset="0" stop-color="#bdff34"/>
7
+ <stop offset="0.58" stop-color="#8ff014"/>
8
+ <stop offset="1" stop-color="#5fd60d"/>
9
+ </linearGradient>
10
+ <linearGradient id="greenWick" x1="168" y1="130" x2="182" y2="366" gradientUnits="userSpaceOnUse">
11
+ <stop offset="0" stop-color="#c8ff3f"/>
12
+ <stop offset="1" stop-color="#67df12"/>
13
+ </linearGradient>
14
+ <linearGradient id="whiteBody" x1="220.9" y1="149" x2="291.1" y2="347" gradientUnits="userSpaceOnUse">
15
+ <stop offset="0" stop-color="#ffffff"/>
16
+ <stop offset="0.62" stop-color="#f3f3f0"/>
17
+ <stop offset="1" stop-color="#dfdfda"/>
18
+ </linearGradient>
19
+ <linearGradient id="whiteWick" x1="249" y1="106" x2="263" y2="390" gradientUnits="userSpaceOnUse">
20
+ <stop offset="0" stop-color="#ffffff"/>
21
+ <stop offset="1" stop-color="#e5e5e1"/>
22
+ </linearGradient>
23
+ <linearGradient id="redBody" x1="305.5" y1="173" x2="368.5" y2="323" gradientUnits="userSpaceOnUse">
24
+ <stop offset="0" stop-color="#ff5f66"/>
25
+ <stop offset="0.55" stop-color="#ff2631"/>
26
+ <stop offset="1" stop-color="#ff1421"/>
27
+ </linearGradient>
28
+ <linearGradient id="redWick" x1="330" y1="130" x2="344" y2="366" gradientUnits="userSpaceOnUse">
29
+ <stop offset="0" stop-color="#ff686e"/>
30
+ <stop offset="1" stop-color="#ff1421"/>
31
+ </linearGradient>
32
+ </defs>
33
+
34
+ <rect width="512" height="512" fill="#252628"/>
35
+
36
+ <g transform="translate(256 248) scale(1.15) translate(-256 -248)">
37
+ <g fill="#000000">
38
+ <g opacity="0.035">
39
+ <rect x="173" y="140" width="22" height="246" rx="11"/>
40
+ <rect x="146.2" y="181" width="75.6" height="164" rx="13"/>
41
+ <rect x="254" y="116" width="22" height="294" rx="11"/>
42
+ <rect x="223.6" y="157" width="82.8" height="212" rx="13"/>
43
+ <rect x="335" y="140" width="22" height="246" rx="11"/>
44
+ <rect x="308.2" y="181" width="75.6" height="164" rx="13"/>
45
+ </g>
46
+ <g opacity="0.06">
47
+ <rect x="173" y="138" width="18" height="242" rx="9"/>
48
+ <rect x="146.9" y="180" width="70.2" height="160" rx="11"/>
49
+ <rect x="254" y="114" width="18" height="290" rx="9"/>
50
+ <rect x="224.3" y="156" width="77.4" height="206" rx="11"/>
51
+ <rect x="335" y="138" width="18" height="242" rx="9"/>
52
+ <rect x="308.9" y="180" width="70.2" height="160" rx="11"/>
53
+ </g>
54
+ <g opacity="0.08">
55
+ <rect x="172" y="137" width="14" height="236" rx="7"/>
56
+ <rect x="147.5" y="180" width="63" height="150" rx="8"/>
57
+ <rect x="253" y="113" width="14" height="284" rx="7"/>
58
+ <rect x="224.9" y="156" width="70.2" height="198" rx="8"/>
59
+ <rect x="334" y="137" width="14" height="236" rx="7"/>
60
+ <rect x="309.5" y="180" width="63" height="150" rx="8"/>
61
+ </g>
62
+ </g>
63
+
64
+ <g>
65
+ <rect x="168" y="130" width="14" height="236" rx="7" fill="url(#greenWick)"/>
66
+ <rect x="143.5" y="173" width="63" height="150" rx="8" fill="url(#greenBody)"/>
67
+ <rect x="249" y="106" width="14" height="284" rx="7" fill="url(#whiteWick)"/>
68
+ <rect x="220.9" y="149" width="70.2" height="198" rx="8" fill="url(#whiteBody)"/>
69
+ <rect x="330" y="130" width="14" height="236" rx="7" fill="url(#redWick)"/>
70
+ <rect x="305.5" y="173" width="63" height="150" rx="8" fill="url(#redBody)"/>
71
+ </g>
72
+ </g>
73
+ </svg>
@@ -0,0 +1,157 @@
1
+ import type { ResourceStore } from "../data/resource-store";
2
+ import type { BrokerAdapter } from "../types/broker";
3
+ import type { BrokerInstanceConfig } from "../types/config";
4
+ import type { CachePolicy } from "../types/persistence";
5
+ import type { BrokerAccount } from "../types/trading";
6
+
7
+ const BROKER_ACCOUNT_SNAPSHOT_KIND = "account-snapshot";
8
+ const BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION = 1;
9
+ const DEFAULT_BROKER_ACCOUNT_CACHE_POLICY = {
10
+ staleMs: 6 * 60 * 60 * 1000,
11
+ expireMs: 30 * 24 * 60 * 60 * 1000,
12
+ } as const satisfies CachePolicy;
13
+
14
+ interface PersistedBrokerAccountSnapshot {
15
+ accounts: BrokerAccount[];
16
+ brokerType?: string;
17
+ }
18
+
19
+ function hashString(input: string): string {
20
+ let hash = 0x811c9dc5;
21
+ for (let index = 0; index < input.length; index += 1) {
22
+ hash ^= input.charCodeAt(index);
23
+ hash = Math.imul(hash, 0x01000193);
24
+ }
25
+ return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
26
+ }
27
+
28
+ function brokerAccountNamespace(instance: BrokerInstanceConfig): string {
29
+ return `plugin:${instance.brokerType}`;
30
+ }
31
+
32
+ export function getBrokerAccountCacheSourceKey(
33
+ instance: BrokerInstanceConfig,
34
+ broker?: BrokerAdapter | null,
35
+ ): string {
36
+ if (broker?.getAccountCacheSourceKey) {
37
+ return broker.getAccountCacheSourceKey(instance);
38
+ }
39
+ return hashString(JSON.stringify({
40
+ brokerType: instance.brokerType,
41
+ config: broker?.toConfigValues?.(instance) ?? instance.config,
42
+ }));
43
+ }
44
+
45
+ function getBrokerAccountCachePolicy(
46
+ instance: BrokerInstanceConfig,
47
+ broker?: BrokerAdapter | null,
48
+ ): CachePolicy {
49
+ return broker?.getAccountCachePolicy?.(instance) ?? DEFAULT_BROKER_ACCOUNT_CACHE_POLICY;
50
+ }
51
+
52
+ function pruneMismatchedSnapshots(
53
+ resources: ResourceStore,
54
+ instance: BrokerInstanceConfig,
55
+ sourceKey: string,
56
+ ): void {
57
+ const records = resources.list<PersistedBrokerAccountSnapshot>({
58
+ namespace: brokerAccountNamespace(instance),
59
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
60
+ entityKey: instance.id,
61
+ }, {
62
+ schemaVersion: BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
63
+ allowExpired: true,
64
+ });
65
+
66
+ for (const record of records) {
67
+ if (record.sourceKey === sourceKey) continue;
68
+ resources.delete({
69
+ namespace: brokerAccountNamespace(instance),
70
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
71
+ entityKey: instance.id,
72
+ sourceKey: record.sourceKey,
73
+ });
74
+ }
75
+ }
76
+
77
+ export function loadPersistedBrokerAccounts(
78
+ resources: ResourceStore,
79
+ instance: BrokerInstanceConfig,
80
+ broker?: BrokerAdapter | null,
81
+ ): BrokerAccount[] | null {
82
+ const sourceKey = getBrokerAccountCacheSourceKey(instance, broker);
83
+ pruneMismatchedSnapshots(resources, instance, sourceKey);
84
+
85
+ return resources.get<PersistedBrokerAccountSnapshot>({
86
+ namespace: brokerAccountNamespace(instance),
87
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
88
+ entityKey: instance.id,
89
+ sourceKey,
90
+ }, {
91
+ schemaVersion: BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
92
+ })?.value.accounts ?? null;
93
+ }
94
+
95
+ export function persistBrokerAccounts(
96
+ resources: ResourceStore,
97
+ instance: BrokerInstanceConfig,
98
+ broker: BrokerAdapter,
99
+ accounts: BrokerAccount[],
100
+ ): void {
101
+ const sourceKey = getBrokerAccountCacheSourceKey(instance, broker);
102
+ pruneMismatchedSnapshots(resources, instance, sourceKey);
103
+
104
+ resources.set<PersistedBrokerAccountSnapshot>({
105
+ namespace: brokerAccountNamespace(instance),
106
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
107
+ entityKey: instance.id,
108
+ sourceKey,
109
+ }, {
110
+ brokerType: instance.brokerType,
111
+ accounts,
112
+ }, {
113
+ schemaVersion: BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
114
+ cachePolicy: getBrokerAccountCachePolicy(instance, broker),
115
+ });
116
+ }
117
+
118
+ export function loadPersistedBrokerAccountMap(
119
+ resources: ResourceStore,
120
+ brokerInstances: BrokerInstanceConfig[],
121
+ brokers: ReadonlyMap<string, BrokerAdapter>,
122
+ ): Record<string, BrokerAccount[]> {
123
+ const accountMap: Record<string, BrokerAccount[]> = {};
124
+
125
+ for (const instance of brokerInstances) {
126
+ const broker = brokers.get(instance.brokerType);
127
+ if (!broker?.listAccounts) continue;
128
+ const accounts = loadPersistedBrokerAccounts(resources, instance, broker);
129
+ if (!accounts || accounts.length === 0) continue;
130
+ accountMap[instance.id] = accounts;
131
+ }
132
+
133
+ return accountMap;
134
+ }
135
+
136
+ export function clearPersistedBrokerAccounts(
137
+ resources: ResourceStore,
138
+ instance: BrokerInstanceConfig,
139
+ ): void {
140
+ const records = resources.list<PersistedBrokerAccountSnapshot>({
141
+ namespace: brokerAccountNamespace(instance),
142
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
143
+ entityKey: instance.id,
144
+ }, {
145
+ schemaVersion: BROKER_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
146
+ allowExpired: true,
147
+ });
148
+
149
+ for (const record of records) {
150
+ resources.delete({
151
+ namespace: brokerAccountNamespace(instance),
152
+ kind: BROKER_ACCOUNT_SNAPSHOT_KIND,
153
+ entityKey: instance.id,
154
+ sourceKey: record.sourceKey,
155
+ });
156
+ }
157
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { BrokerAdapter } from "../types/broker";
3
+ import type { BrokerInstanceConfig } from "../types/config";
4
+ import { ibkrBroker } from "../plugins/ibkr/broker-adapter";
5
+ import {
6
+ buildBrokerProfileConfig,
7
+ createBrokerProfileDraft,
8
+ validateBrokerProfileValues,
9
+ } from "./profile-form";
10
+
11
+ function createFlexInstance(): BrokerInstanceConfig {
12
+ return {
13
+ id: "ibkr-flex",
14
+ brokerType: "ibkr",
15
+ label: "IBKR Flex",
16
+ connectionMode: "flex",
17
+ config: {
18
+ connectionMode: "flex",
19
+ gatewaySetupMode: "auto",
20
+ flex: { token: "saved-token", queryId: "123", endpoint: "https://example.test/flex" },
21
+ gateway: { host: "127.0.0.1", marketDataType: "auto" },
22
+ },
23
+ enabled: true,
24
+ };
25
+ }
26
+
27
+ describe("broker profile form helpers", () => {
28
+ test("builds canonical IBKR Flex config from flat values", () => {
29
+ const config = buildBrokerProfileConfig(ibkrBroker, {
30
+ connectionMode: "flex",
31
+ token: "token",
32
+ queryId: "456",
33
+ endpoint: "",
34
+ gatewaySetupMode: "auto",
35
+ });
36
+
37
+ expect(config).toMatchObject({
38
+ connectionMode: "flex",
39
+ flex: { token: "token", queryId: "456" },
40
+ });
41
+ });
42
+
43
+ test("preserves saved password fields when editing leaves them blank", () => {
44
+ const previous = createFlexInstance();
45
+ const draft = createBrokerProfileDraft(ibkrBroker, previous);
46
+ draft.values.token = "";
47
+
48
+ expect(validateBrokerProfileValues(ibkrBroker, draft.values, previous)).toBeNull();
49
+ expect(buildBrokerProfileConfig(ibkrBroker, draft.values, previous)).toMatchObject({
50
+ flex: { token: "saved-token" },
51
+ });
52
+ });
53
+
54
+ test("requires password fields for new profiles", () => {
55
+ expect(validateBrokerProfileValues(ibkrBroker, {
56
+ connectionMode: "flex",
57
+ token: "",
58
+ queryId: "123",
59
+ })).toBe("Flex Token is required.");
60
+ });
61
+
62
+ test("falls back to raw values for generic brokers", () => {
63
+ const adapter: BrokerAdapter = {
64
+ id: "demo",
65
+ name: "Demo",
66
+ configSchema: [{ key: "apiKey", label: "API Key", type: "password", required: true }],
67
+ validate: async () => true,
68
+ importPositions: async () => [],
69
+ };
70
+
71
+ expect(buildBrokerProfileConfig(adapter, { apiKey: "secret" })).toEqual({ apiKey: "secret" });
72
+ });
73
+ });
@@ -0,0 +1,109 @@
1
+ import type { BrokerAdapter, BrokerConfigField } from "../types/broker";
2
+ import type { BrokerInstanceConfig } from "../types/config";
3
+ import { resolveBrokerConfigFields } from "../types/broker";
4
+
5
+ export interface BrokerProfileDraft {
6
+ label: string;
7
+ enabled: boolean;
8
+ values: Record<string, string>;
9
+ }
10
+
11
+ export const PRESERVED_PASSWORD_HINT = "Saved; leave blank to keep";
12
+
13
+ function fieldDefaultValue(field: BrokerConfigField): string {
14
+ if (field.defaultValue != null) return field.defaultValue;
15
+ if (field.type === "select" && field.options?.[0]?.value) return field.options[0].value;
16
+ return "";
17
+ }
18
+
19
+ function normalizeFormValue(value: unknown): string {
20
+ if (value == null) return "";
21
+ if (typeof value === "string") return value;
22
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
23
+ return "";
24
+ }
25
+
26
+ export function flattenBrokerConfigValues(
27
+ adapter: BrokerAdapter,
28
+ instance?: BrokerInstanceConfig,
29
+ ): Record<string, string> {
30
+ const raw = instance
31
+ ? adapter.toConfigValues?.(instance) ?? instance.config
32
+ : {};
33
+ const values: Record<string, string> = {};
34
+
35
+ for (const field of adapter.configSchema) {
36
+ values[field.key] = normalizeFormValue(raw[field.key] ?? fieldDefaultValue(field));
37
+ }
38
+
39
+ return values;
40
+ }
41
+
42
+ export function createBrokerProfileDraft(
43
+ adapter: BrokerAdapter,
44
+ instance?: BrokerInstanceConfig,
45
+ ): BrokerProfileDraft {
46
+ return {
47
+ label: instance?.label || adapter.name,
48
+ enabled: instance?.enabled !== false,
49
+ values: flattenBrokerConfigValues(adapter, instance),
50
+ };
51
+ }
52
+
53
+ export function getVisibleBrokerConfigFields(
54
+ adapter: BrokerAdapter,
55
+ values: Record<string, unknown>,
56
+ ): BrokerConfigField[] {
57
+ return resolveBrokerConfigFields(adapter, values);
58
+ }
59
+
60
+ function getPreviousPasswordValue(
61
+ adapter: BrokerAdapter,
62
+ previous: BrokerInstanceConfig | undefined,
63
+ key: string,
64
+ ): string {
65
+ if (!previous) return "";
66
+ return normalizeFormValue((adapter.toConfigValues?.(previous) ?? previous.config)[key]);
67
+ }
68
+
69
+ export function withPreservedBrokerPasswords(
70
+ adapter: BrokerAdapter,
71
+ values: Record<string, string>,
72
+ previous?: BrokerInstanceConfig,
73
+ ): Record<string, string> {
74
+ const next = { ...values };
75
+ if (!previous) return next;
76
+
77
+ for (const field of adapter.configSchema) {
78
+ if (field.type !== "password") continue;
79
+ if (next[field.key]?.trim()) continue;
80
+ const previousValue = getPreviousPasswordValue(adapter, previous, field.key);
81
+ if (previousValue) next[field.key] = previousValue;
82
+ }
83
+
84
+ return next;
85
+ }
86
+
87
+ export function validateBrokerProfileValues(
88
+ adapter: BrokerAdapter,
89
+ values: Record<string, string>,
90
+ previous?: BrokerInstanceConfig,
91
+ ): string | null {
92
+ const resolvedValues = withPreservedBrokerPasswords(adapter, values, previous);
93
+ for (const field of getVisibleBrokerConfigFields(adapter, resolvedValues)) {
94
+ if (!field.required) continue;
95
+ if (!normalizeFormValue(resolvedValues[field.key]).trim()) {
96
+ return `${field.label} is required.`;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ export function buildBrokerProfileConfig(
103
+ adapter: BrokerAdapter,
104
+ values: Record<string, string>,
105
+ previous?: BrokerInstanceConfig,
106
+ ): Record<string, unknown> {
107
+ const resolvedValues = withPreservedBrokerPasswords(adapter, values, previous);
108
+ return adapter.fromConfigValues?.(resolvedValues, previous) ?? resolvedValues;
109
+ }
@@ -0,0 +1,91 @@
1
+ import type { TimeRange } from "../components/chart/chart-types";
2
+ import type { ManualChartResolution } from "../components/chart/chart-resolution";
3
+ import type { BrokerAdapter, BrokerConnectionStatus } from "../types/broker";
4
+ import type { BrokerInstanceConfig } from "../types/config";
5
+ import type { QuoteSubscriptionTarget } from "../types/data-provider";
6
+ import type { BrokerContractRef } from "../types/instrument";
7
+ import type { BrokerOrderRequest } from "../types/trading";
8
+
9
+ export interface BrokerRemoteClient {
10
+ invoke<T = unknown>(instanceId: string, operation: string, args?: unknown[]): Promise<T>;
11
+ getStatus(instanceId: string): BrokerConnectionStatus | null;
12
+ subscribeStatus(instanceId: string, listener: () => void): () => void;
13
+ subscribeQuotes(
14
+ instanceId: string,
15
+ targets: QuoteSubscriptionTarget[],
16
+ onQuote: (target: QuoteSubscriptionTarget, quote: any) => void,
17
+ ): () => void;
18
+ removeInstance(instanceId: string): Promise<void>;
19
+ destroyAll(): Promise<void>;
20
+ }
21
+
22
+ let brokerRemoteClient: BrokerRemoteClient | null = null;
23
+
24
+ export function setBrokerRemoteClient(client: BrokerRemoteClient | null): void {
25
+ brokerRemoteClient = client;
26
+ }
27
+
28
+ export function getBrokerRemoteClient(): BrokerRemoteClient | null {
29
+ return brokerRemoteClient;
30
+ }
31
+
32
+ function disconnectedStatus(): BrokerConnectionStatus {
33
+ return { state: "disconnected", updatedAt: 0 };
34
+ }
35
+
36
+ export function createRemoteBrokerAdapter(adapter: BrokerAdapter): BrokerAdapter {
37
+ const invoke = <T,>(instance: BrokerInstanceConfig, operation: string, args: unknown[] = []) => {
38
+ const client = getBrokerRemoteClient();
39
+ if (!client) throw new Error("Broker remote host is not available.");
40
+ return client.invoke<T>(instance.id, operation, args);
41
+ };
42
+
43
+ return {
44
+ ...adapter,
45
+ validate: (instance) => invoke<boolean>(instance, "validate"),
46
+ importPositions: (instance) => invoke(instance, "importPositions"),
47
+ connect: (instance) => invoke<void>(instance, "connect"),
48
+ disconnect: (instance) => invoke<void>(instance, "disconnect"),
49
+ getStatus: (instance) => getBrokerRemoteClient()?.getStatus(instance.id) ?? adapter.getStatus?.(instance) ?? disconnectedStatus(),
50
+ subscribeStatus: (instance, listener) => getBrokerRemoteClient()?.subscribeStatus(instance.id, listener) ?? (() => {}),
51
+ getPersistedConfigUpdate: (instance) => invoke<Record<string, unknown> | null>(instance, "getPersistedConfigUpdate"),
52
+ listAccounts: (instance) => invoke(instance, "listAccounts"),
53
+ searchInstruments: (query, instance) => invoke(instance, "searchInstruments", [query]),
54
+ getTickerFinancials: (ticker, instance, exchange?: string, instrument?: BrokerContractRef | null) =>
55
+ invoke(instance, "getTickerFinancials", [ticker, exchange, instrument]),
56
+ getQuote: (ticker, instance, exchange?: string, instrument?: BrokerContractRef | null) =>
57
+ invoke(instance, "getQuote", [ticker, exchange, instrument]),
58
+ getPriceHistory: (ticker, instance, exchange: string, range: TimeRange, instrument?: BrokerContractRef | null) =>
59
+ invoke(instance, "getPriceHistory", [ticker, exchange, range, instrument]),
60
+ getPriceHistoryForResolution: (
61
+ ticker,
62
+ instance,
63
+ exchange: string,
64
+ bufferRange: TimeRange,
65
+ resolution: ManualChartResolution,
66
+ instrument?: BrokerContractRef | null,
67
+ ) => invoke(instance, "getPriceHistoryForResolution", [ticker, exchange, bufferRange, resolution, instrument]),
68
+ getDetailedPriceHistory: (
69
+ ticker,
70
+ instance,
71
+ exchange: string,
72
+ startDate: Date,
73
+ endDate: Date,
74
+ barSize: string,
75
+ instrument?: BrokerContractRef | null,
76
+ ) => invoke(instance, "getDetailedPriceHistory", [ticker, exchange, startDate, endDate, barSize, instrument]),
77
+ getChartResolutionSupport: (ticker, instance, exchange?: string, instrument?: BrokerContractRef | null) =>
78
+ invoke(instance, "getChartResolutionSupport", [ticker, exchange, instrument]),
79
+ getChartResolutionCapabilities: (ticker, instance, exchange?: string, instrument?: BrokerContractRef | null) =>
80
+ invoke(instance, "getChartResolutionCapabilities", [ticker, exchange, instrument]),
81
+ getOptionsChain: (ticker, instance, exchange?: string, expirationDate?: number, instrument?: BrokerContractRef | null) =>
82
+ invoke(instance, "getOptionsChain", [ticker, exchange, expirationDate, instrument]),
83
+ subscribeQuotes: (instance, targets, onQuote) => getBrokerRemoteClient()?.subscribeQuotes(instance.id, targets, onQuote) ?? (() => {}),
84
+ listOpenOrders: (instance) => invoke(instance, "listOpenOrders"),
85
+ listExecutions: (instance) => invoke(instance, "listExecutions"),
86
+ previewOrder: (instance, request: BrokerOrderRequest) => invoke(instance, "previewOrder", [request]),
87
+ placeOrder: (instance, request: BrokerOrderRequest) => invoke(instance, "placeOrder", [request]),
88
+ modifyOrder: (instance, orderId: number, request: BrokerOrderRequest) => invoke(instance, "modifyOrder", [orderId, request]),
89
+ cancelOrder: (instance, orderId: number) => invoke<void>(instance, "cancelOrder", [orderId]),
90
+ };
91
+ }