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
@@ -2,15 +2,16 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { existsSync, rmSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
+ import {
6
+ clearPersistedBrokerAccounts,
7
+ loadPersistedBrokerAccountMap,
8
+ loadPersistedBrokerAccounts,
9
+ persistBrokerAccounts,
10
+ } from "../../brokers/account-cache";
5
11
  import { AppPersistence } from "../../data/app-persistence";
6
12
  import type { BrokerInstanceConfig } from "../../types/config";
7
13
  import type { BrokerAccount } from "../../types/trading";
8
- import {
9
- clearPersistedIbkrAccounts,
10
- loadPersistedIbkrAccountMap,
11
- loadPersistedIbkrAccounts,
12
- persistIbkrAccounts,
13
- } from "./account-cache";
14
+ import { ibkrBroker } from "./broker-adapter";
14
15
 
15
16
  const tempPaths: string[] = [];
16
17
 
@@ -55,10 +56,10 @@ describe("IBKR account cache", () => {
55
56
  const persistence = new AppPersistence(createTempDbPath("ibkr-account-cache"));
56
57
  const instance = createIbkrInstance("flex", { id: "ibkr-personal" });
57
58
 
58
- persistIbkrAccounts(persistence.resources, instance, TEST_ACCOUNTS);
59
+ persistBrokerAccounts(persistence.resources, instance, ibkrBroker, TEST_ACCOUNTS);
59
60
 
60
- expect(loadPersistedIbkrAccounts(persistence.resources, instance)).toEqual(TEST_ACCOUNTS);
61
- expect(loadPersistedIbkrAccountMap(persistence.resources, [instance])).toEqual({
61
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, ibkrBroker)).toEqual(TEST_ACCOUNTS);
62
+ expect(loadPersistedBrokerAccountMap(persistence.resources, [instance], new Map([["ibkr", ibkrBroker]]))).toEqual({
62
63
  "ibkr-personal": TEST_ACCOUNTS,
63
64
  });
64
65
 
@@ -73,9 +74,9 @@ describe("IBKR account cache", () => {
73
74
  config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4003, clientId: 1 } },
74
75
  });
75
76
 
76
- persistIbkrAccounts(persistence.resources, original, TEST_ACCOUNTS);
77
+ persistBrokerAccounts(persistence.resources, original, ibkrBroker, TEST_ACCOUNTS);
77
78
 
78
- expect(loadPersistedIbkrAccounts(persistence.resources, updated)).toBeNull();
79
+ expect(loadPersistedBrokerAccounts(persistence.resources, updated, ibkrBroker)).toBeNull();
79
80
  expect(persistence.resources.list({
80
81
  namespace: "plugin:ibkr",
81
82
  kind: "account-snapshot",
@@ -92,10 +93,10 @@ describe("IBKR account cache", () => {
92
93
  const persistence = new AppPersistence(createTempDbPath("ibkr-account-cache-clear"));
93
94
  const instance = createIbkrInstance("gateway", { id: "ibkr-live" });
94
95
 
95
- persistIbkrAccounts(persistence.resources, instance, TEST_ACCOUNTS);
96
- clearPersistedIbkrAccounts(persistence.resources, instance.id);
96
+ persistBrokerAccounts(persistence.resources, instance, ibkrBroker, TEST_ACCOUNTS);
97
+ clearPersistedBrokerAccounts(persistence.resources, instance);
97
98
 
98
- expect(loadPersistedIbkrAccounts(persistence.resources, instance)).toBeNull();
99
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, ibkrBroker)).toBeNull();
99
100
  persistence.close();
100
101
  });
101
102
  });
@@ -1,137 +1,32 @@
1
- import { createHash } from "crypto";
2
- import type { ResourceStore } from "../../data/resource-store";
3
1
  import type { BrokerInstanceConfig } from "../../types/config";
4
- import type { BrokerAccount } from "../../types/trading";
5
- import { normalizeIbkrConfig, type IbkrConnectionMode } from "./config";
6
-
7
- const IBKR_PLUGIN_NAMESPACE = "plugin:ibkr";
8
- const IBKR_ACCOUNT_SNAPSHOT_KIND = "account-snapshot";
9
- const IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION = 1;
2
+ import type { CachePolicy } from "../../types/persistence";
3
+ import { normalizeIbkrConfig } from "./config";
10
4
 
11
5
  const FLEX_ACCOUNT_CACHE_POLICY = {
12
6
  staleMs: 6 * 60 * 60 * 1000,
13
7
  expireMs: 30 * 24 * 60 * 60 * 1000,
14
- } as const;
8
+ } as const satisfies CachePolicy;
15
9
 
16
10
  const GATEWAY_ACCOUNT_CACHE_POLICY = {
17
11
  staleMs: 30 * 1000,
18
12
  expireMs: 7 * 24 * 60 * 60 * 1000,
19
- } as const;
13
+ } as const satisfies CachePolicy;
20
14
 
21
- interface PersistedIbkrAccountSnapshot {
22
- connectionMode: IbkrConnectionMode;
23
- accounts: BrokerAccount[];
15
+ function hashString(input: string): string {
16
+ let hash = 0x811c9dc5;
17
+ for (let index = 0; index < input.length; index += 1) {
18
+ hash ^= input.charCodeAt(index);
19
+ hash = Math.imul(hash, 0x01000193);
20
+ }
21
+ return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
24
22
  }
25
23
 
26
- function getIbkrAccountSnapshotSourceKey(instance: BrokerInstanceConfig): string {
27
- const normalized = normalizeIbkrConfig(instance.config);
28
- return createHash("sha256")
29
- .update(JSON.stringify(normalized))
30
- .digest("hex");
24
+ export function getIbkrAccountCacheSourceKey(instance: BrokerInstanceConfig): string {
25
+ return hashString(JSON.stringify(normalizeIbkrConfig(instance.config)));
31
26
  }
32
27
 
33
- function getAccountCachePolicy(instance: BrokerInstanceConfig) {
34
- const normalized = normalizeIbkrConfig(instance.config);
35
- return normalized.connectionMode === "gateway"
28
+ export function getIbkrAccountCachePolicy(instance: BrokerInstanceConfig): CachePolicy {
29
+ return normalizeIbkrConfig(instance.config).connectionMode === "gateway"
36
30
  ? GATEWAY_ACCOUNT_CACHE_POLICY
37
31
  : FLEX_ACCOUNT_CACHE_POLICY;
38
32
  }
39
-
40
- function pruneMismatchedSnapshots(resources: ResourceStore, instance: BrokerInstanceConfig, sourceKey: string): void {
41
- const records = resources.list<PersistedIbkrAccountSnapshot>({
42
- namespace: IBKR_PLUGIN_NAMESPACE,
43
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
44
- entityKey: instance.id,
45
- }, {
46
- schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
47
- allowExpired: true,
48
- });
49
-
50
- for (const record of records) {
51
- if (record.sourceKey === sourceKey) continue;
52
- resources.delete({
53
- namespace: IBKR_PLUGIN_NAMESPACE,
54
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
55
- entityKey: instance.id,
56
- sourceKey: record.sourceKey,
57
- });
58
- }
59
- }
60
-
61
- export function loadPersistedIbkrAccounts(
62
- resources: ResourceStore,
63
- instance: BrokerInstanceConfig,
64
- ): BrokerAccount[] | null {
65
- const sourceKey = getIbkrAccountSnapshotSourceKey(instance);
66
- pruneMismatchedSnapshots(resources, instance, sourceKey);
67
-
68
- return resources.get<PersistedIbkrAccountSnapshot>({
69
- namespace: IBKR_PLUGIN_NAMESPACE,
70
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
71
- entityKey: instance.id,
72
- sourceKey,
73
- }, {
74
- schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
75
- })?.value.accounts ?? null;
76
- }
77
-
78
- export function persistIbkrAccounts(
79
- resources: ResourceStore,
80
- instance: BrokerInstanceConfig,
81
- accounts: BrokerAccount[],
82
- ): void {
83
- const sourceKey = getIbkrAccountSnapshotSourceKey(instance);
84
- pruneMismatchedSnapshots(resources, instance, sourceKey);
85
-
86
- resources.set<PersistedIbkrAccountSnapshot>({
87
- namespace: IBKR_PLUGIN_NAMESPACE,
88
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
89
- entityKey: instance.id,
90
- sourceKey,
91
- }, {
92
- connectionMode: normalizeIbkrConfig(instance.config).connectionMode,
93
- accounts,
94
- }, {
95
- schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
96
- cachePolicy: getAccountCachePolicy(instance),
97
- });
98
- }
99
-
100
- export function loadPersistedIbkrAccountMap(
101
- resources: ResourceStore,
102
- brokerInstances: BrokerInstanceConfig[],
103
- ): Record<string, BrokerAccount[]> {
104
- const accountMap: Record<string, BrokerAccount[]> = {};
105
-
106
- for (const instance of brokerInstances) {
107
- if (instance.brokerType !== "ibkr") continue;
108
- const accounts = loadPersistedIbkrAccounts(resources, instance);
109
- if (!accounts || accounts.length === 0) continue;
110
- accountMap[instance.id] = accounts;
111
- }
112
-
113
- return accountMap;
114
- }
115
-
116
- export function clearPersistedIbkrAccounts(
117
- resources: ResourceStore,
118
- instanceId: string,
119
- ): void {
120
- const records = resources.list<PersistedIbkrAccountSnapshot>({
121
- namespace: IBKR_PLUGIN_NAMESPACE,
122
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
123
- entityKey: instanceId,
124
- }, {
125
- schemaVersion: IBKR_ACCOUNT_SNAPSHOT_SCHEMA_VERSION,
126
- allowExpired: true,
127
- });
128
-
129
- for (const record of records) {
130
- resources.delete({
131
- namespace: IBKR_PLUGIN_NAMESPACE,
132
- kind: IBKR_ACCOUNT_SNAPSHOT_KIND,
133
- entityKey: instanceId,
134
- sourceKey: record.sourceKey,
135
- });
136
- }
137
- }
@@ -1,6 +1,7 @@
1
1
  import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
2
2
  import type { BrokerInstanceConfig } from "../../types/config";
3
3
  import {
4
+ buildIbkrConfigFromValues,
4
5
  getGatewayConfig,
5
6
  IBKR_CONFIG_FIELDS,
6
7
  isFlexConfigured,
@@ -8,6 +9,7 @@ import {
8
9
  normalizeIbkrConfig,
9
10
  type FlexQueryConfig,
10
11
  } from "./config";
12
+ import { getIbkrAccountCachePolicy, getIbkrAccountCacheSourceKey } from "./account-cache";
11
13
  import { loadFlexStatement, parseFlexAccounts, parseFlexPositions } from "./flex";
12
14
  import { ibkrGatewayManager } from "./gateway-service";
13
15
  import { refreshGatewayData } from "./gateway-helpers";
@@ -49,9 +51,74 @@ export const ibkrBroker: BrokerAdapter = {
49
51
  },
50
52
 
51
53
  getStatus(instance) {
54
+ const normalized = normalizeIbkrConfig(instance.config);
55
+ if (normalized.connectionMode !== "gateway") {
56
+ return {
57
+ state: "disconnected",
58
+ message: "Flex profiles sync on demand",
59
+ mode: "flex",
60
+ updatedAt: 0,
61
+ };
62
+ }
52
63
  return { ...ibkrGatewayManager.getSnapshot(instance.id).status, mode: "gateway" };
53
64
  },
54
65
 
66
+ subscribeStatus(instance, listener) {
67
+ return ibkrGatewayManager.subscribe(instance.id, listener);
68
+ },
69
+
70
+ getPersistedConfigUpdate(instance) {
71
+ const normalized = normalizeIbkrConfig(instance.config);
72
+ if (normalized.connectionMode !== "gateway") return null;
73
+ const resolved = ibkrGatewayManager.getService(instance.id).getResolvedConnection();
74
+ return resolved ? buildPersistedIbkrGatewayConfig(instance.config, resolved) : null;
75
+ },
76
+
77
+ getAccountCacheSourceKey: getIbkrAccountCacheSourceKey,
78
+ getAccountCachePolicy: getIbkrAccountCachePolicy,
79
+
80
+ getProfileActions(instance) {
81
+ const normalized = normalizeIbkrConfig(instance.config);
82
+ return [{
83
+ id: "ibkr-console",
84
+ label: "IBKR Console",
85
+ widgetId: "ibkr-trading",
86
+ disabled: normalized.connectionMode !== "gateway",
87
+ disabledReason: "IBKR Console is available for Gateway / TWS profiles.",
88
+ }];
89
+ },
90
+
91
+ toConfigValues(instance) {
92
+ const normalized = normalizeIbkrConfig(instance.config);
93
+ return {
94
+ connectionMode: normalized.connectionMode,
95
+ token: normalized.flex.token,
96
+ queryId: normalized.flex.queryId,
97
+ endpoint: normalized.flex.endpoint,
98
+ gatewaySetupMode: normalized.gatewaySetupMode,
99
+ host: normalized.gateway.host,
100
+ port: normalized.gateway.port,
101
+ clientId: normalized.gateway.clientId,
102
+ marketDataType: normalized.gateway.marketDataType,
103
+ };
104
+ },
105
+
106
+ fromConfigValues(values, previous) {
107
+ const next = buildIbkrConfigFromValues(values);
108
+ const previousConfig = previous ? normalizeIbkrConfig(previous.config) : null;
109
+ if (!previousConfig || next.connectionMode !== "gateway") return next;
110
+
111
+ return {
112
+ ...next,
113
+ gateway: {
114
+ ...next.gateway,
115
+ marketDataType: next.gateway.marketDataType ?? previousConfig.gateway.marketDataType,
116
+ lastSuccessfulPort: previousConfig.gateway.lastSuccessfulPort,
117
+ lastSuccessfulClientId: previousConfig.gateway.lastSuccessfulClientId,
118
+ },
119
+ };
120
+ },
121
+
55
122
  async listAccounts(instance) {
56
123
  const normalized = normalizeIbkrConfig(instance.config);
57
124
  if (normalized.connectionMode === "gateway") {
@@ -22,24 +22,6 @@ describe("ibkr config helpers", () => {
22
22
  });
23
23
  });
24
24
 
25
- test("defaults gateway profiles to automatic connection detection", () => {
26
- const config = buildIbkrConfigFromValues({
27
- connectionMode: "gateway",
28
- gatewaySetupMode: "auto",
29
- });
30
-
31
- expect(config.connectionMode).toBe("gateway");
32
- expect(config.gatewaySetupMode).toBe("auto");
33
- expect(config.gateway).toEqual({
34
- host: "127.0.0.1",
35
- port: undefined,
36
- clientId: undefined,
37
- lastSuccessfulPort: undefined,
38
- lastSuccessfulClientId: undefined,
39
- marketDataType: "auto",
40
- });
41
- });
42
-
43
25
  test("treats explicit port/client configs as manual at runtime", () => {
44
26
  const config = normalizeIbkrConfig({
45
27
  connectionMode: "gateway",
@@ -1,10 +1,14 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import type { InputRenderable } from "@opentui/core";
1
+ import { Box, Input, Text } from "../../ui";
2
+ import { TextAttributes } from "../../ui";
3
+ import { type InputRenderable } from "../../ui";
3
4
  import { useEffect, useRef, useState } from "react";
4
- import { useDialogKeyboard, type PromptContext } from "@opentui-ui/dialog/react";
5
+ import { type PromptContext } from "../../ui/dialog";
5
6
  import type { WizardStep } from "../../types/plugin";
6
7
  import { colors } from "../../theme/colors";
7
8
 
9
+ export { ChoiceDialog } from "../../components";
10
+ export type { ChoiceDialogChoice, ChoiceDialogProps } from "../../components";
11
+
8
12
  export function InputDialog({ resolve, step }: PromptContext<string> & { step: WizardStep }) {
9
13
  const inputRef = useRef<InputRenderable>(null);
10
14
  const [value, setValue] = useState("");
@@ -14,14 +18,14 @@ export function InputDialog({ resolve, step }: PromptContext<string> & { step: W
14
18
  }, []);
15
19
 
16
20
  return (
17
- <box flexDirection="column">
18
- <text attributes={TextAttributes.BOLD} fg={colors.text}>{step.label}</text>
19
- <box height={1} />
21
+ <Box flexDirection="column">
22
+ <Text attributes={TextAttributes.BOLD} fg={colors.text}>{step.label}</Text>
23
+ <Box height={1} />
20
24
  {step.body?.map((line, index) => (
21
- <text key={index} fg={colors.textDim}>{line || " "}</text>
25
+ <Text key={index} fg={colors.textDim}>{line || " "}</Text>
22
26
  ))}
23
- <box height={1} />
24
- <input
27
+ <Box height={1} />
28
+ <Input
25
29
  ref={inputRef}
26
30
  focused
27
31
  placeholder={step.placeholder || ""}
@@ -32,52 +36,6 @@ export function InputDialog({ resolve, step }: PromptContext<string> & { step: W
32
36
  onChange={(nextValue) => setValue(nextValue)}
33
37
  onSubmit={() => resolve(value.trim())}
34
38
  />
35
- </box>
36
- );
37
- }
38
-
39
- export function ChoiceDialog({
40
- resolve,
41
- dialogId,
42
- title,
43
- choices,
44
- }: PromptContext<string> & { title: string; choices: Array<{ id: string; label: string; desc: string }> }) {
45
- const [index, setIndex] = useState(0);
46
-
47
- useDialogKeyboard((event) => {
48
- event.stopPropagation();
49
- if (event.name === "up" || event.name === "k") setIndex((current) => Math.max(0, current - 1));
50
- else if (event.name === "down" || event.name === "j") setIndex((current) => Math.min(choices.length - 1, current + 1));
51
- else if (event.name === "return") resolve(choices[index]!.id);
52
- else if (event.name === "escape") resolve("");
53
- }, dialogId);
54
-
55
- return (
56
- <box flexDirection="column">
57
- <text attributes={TextAttributes.BOLD} fg={colors.text}>{title}</text>
58
- <box height={1} />
59
- {choices.map((choice, choiceIndex) => {
60
- const selected = choiceIndex === index;
61
- return (
62
- <box
63
- key={choice.id}
64
- flexDirection="row"
65
- height={1}
66
- backgroundColor={selected ? colors.selected : colors.bg}
67
- onMouseMove={() => setIndex(choiceIndex)}
68
- onMouseDown={() => resolve(choice.id)}
69
- >
70
- <text fg={selected ? colors.selectedText : colors.textDim}>{selected ? "▸ " : " "}</text>
71
- <text fg={selected ? colors.text : colors.textDim} attributes={selected ? TextAttributes.BOLD : 0}>
72
- {choice.label}
73
- </text>
74
- </box>
75
- );
76
- })}
77
- <box height={1} />
78
- <text fg={colors.textDim}>{choices[index]?.desc || ""}</text>
79
- <box height={1} />
80
- <text fg={colors.textMuted}>↑↓ choose · Enter/click select · Esc cancel</text>
81
- </box>
39
+ </Box>
82
40
  );
83
41
  }
@@ -1,5 +1,13 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { parseFlexAccounts, parseFlexPositions } from "./flex";
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { setHttpFetchTransport } from "../../utils/http-transport";
3
+ import { parseFlexAccounts, parseFlexPositions, requestFlexStatement } from "./flex";
4
+
5
+ const originalFetch = globalThis.fetch;
6
+
7
+ afterEach(() => {
8
+ setHttpFetchTransport(null);
9
+ globalThis.fetch = originalFetch;
10
+ });
3
11
 
4
12
  describe("parseFlexPositions", () => {
5
13
  test("parses option positions with broker contract metadata", () => {
@@ -105,3 +113,50 @@ describe("parseFlexAccounts", () => {
105
113
  ]);
106
114
  });
107
115
  });
116
+
117
+ describe("requestFlexStatement", () => {
118
+ test("uses the configured HTTP transport for statement requests", async () => {
119
+ const requests: string[] = [];
120
+ setHttpFetchTransport(async (url) => {
121
+ requests.push(url);
122
+ return new Response(
123
+ "<FlexStatementResponse><ReferenceCode>987654</ReferenceCode></FlexStatementResponse>",
124
+ { status: 200 },
125
+ );
126
+ });
127
+
128
+ await expect(requestFlexStatement({
129
+ token: "secret-flex-token",
130
+ queryId: "12345",
131
+ endpoint: "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest",
132
+ })).resolves.toBe("987654");
133
+
134
+ expect(requests).toHaveLength(1);
135
+ expect(requests[0]).toContain("q=12345");
136
+ });
137
+
138
+ test("adds request context to vague IBKR Flex errors without exposing the token", async () => {
139
+ globalThis.fetch = (async () => new Response(
140
+ "<FlexStatementResponse><ErrorMessage>Load failed</ErrorMessage></FlexStatementResponse>",
141
+ { status: 200 },
142
+ )) as typeof fetch;
143
+
144
+ let message = "";
145
+ try {
146
+ await requestFlexStatement({
147
+ token: "secret-flex-token",
148
+ queryId: "12345",
149
+ endpoint: "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest",
150
+ });
151
+ } catch (error) {
152
+ message = error instanceof Error ? error.message : String(error);
153
+ }
154
+
155
+ expect(message).toContain("IBKR Flex request failed while requesting the statement: Load failed.");
156
+ expect(message).toContain("Endpoint FlexStatementService.SendRequest");
157
+ expect(message).toContain("query ID 12345");
158
+ expect(message).toContain("token configured");
159
+ expect(message).toContain("Flex Web Service is enabled");
160
+ expect(message).not.toContain("secret-flex-token");
161
+ });
162
+ });
@@ -1,6 +1,7 @@
1
1
  import type { BrokerPosition } from "../../types/broker";
2
2
  import type { BrokerContractRef } from "../../types/instrument";
3
3
  import type { BrokerAccount, BrokerCashBalance } from "../../types/trading";
4
+ import { httpFetch } from "../../utils/http-transport";
4
5
  import type { FlexQueryConfig } from "./config";
5
6
  import { IBKR_STATEMENT_URL } from "./config";
6
7
 
@@ -8,6 +9,17 @@ const IBKR_STATEMENT_GET_URL = "https://gdcdyn.interactivebrokers.com/Universal/
8
9
  const FLEX_STATEMENT_CACHE_MS = 15_000;
9
10
  const flexStatementCache = new Map<string, { createdAt: number; promise: Promise<string> }>();
10
11
 
12
+ type FlexRequestPhase = "request" | "download";
13
+
14
+ interface FlexErrorContext {
15
+ phase: FlexRequestPhase;
16
+ endpoint?: string;
17
+ queryId?: string;
18
+ token?: string;
19
+ referenceCode?: string;
20
+ httpStatus?: number;
21
+ }
22
+
11
23
  function parseAttributes(raw: string): Record<string, string> {
12
24
  const attributes: Record<string, string> = {};
13
25
  for (const [, key, value] of raw.matchAll(/([a-zA-Z0-9]+)="([^"]*)"/g)) {
@@ -46,28 +58,107 @@ function parseFlexTimestamp(raw: string | undefined, fallbackDate: string | unde
46
58
  return new Date(Number(year), Number(month) - 1, Number(day)).getTime();
47
59
  }
48
60
 
61
+ function decodeXmlText(value: string): string {
62
+ return value
63
+ .replace(/&amp;/g, "&")
64
+ .replace(/&lt;/g, "<")
65
+ .replace(/&gt;/g, ">")
66
+ .replace(/&quot;/g, "\"")
67
+ .replace(/&apos;/g, "'");
68
+ }
69
+
70
+ function extractFlexErrorMessage(text: string): string | null {
71
+ const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
72
+ if (errorMatch?.[1]) return decodeXmlText(errorMatch[1].trim());
73
+ return null;
74
+ }
75
+
76
+ function endpointName(endpoint: string | undefined): string {
77
+ if (!endpoint) return "default";
78
+ try {
79
+ const url = new URL(endpoint);
80
+ return url.pathname.split("/").filter(Boolean).at(-1) || url.hostname;
81
+ } catch {
82
+ return endpoint.split("?")[0]?.split("/").filter(Boolean).at(-1) || endpoint;
83
+ }
84
+ }
85
+
86
+ function buildFlexErrorMessage(providerMessage: string, context: FlexErrorContext): string {
87
+ const phase = context.phase === "request"
88
+ ? "requesting the statement"
89
+ : "downloading the generated statement";
90
+ const details = [
91
+ `Endpoint ${endpointName(context.endpoint)}`,
92
+ context.queryId ? `query ID ${context.queryId}` : null,
93
+ context.referenceCode ? `reference code ${context.referenceCode}` : null,
94
+ context.token ? "token configured" : "token missing",
95
+ context.httpStatus && context.httpStatus >= 400 ? `HTTP ${context.httpStatus}` : null,
96
+ ].filter(Boolean).join(", ");
97
+ const advice = context.phase === "request"
98
+ ? "Check that Flex Web Service is enabled, the token is active, and the query ID belongs to that token."
99
+ : "IBKR accepted the request but did not return the statement; retry shortly or check that the Flex query can generate a statement.";
100
+
101
+ return `IBKR Flex request failed while ${phase}: ${providerMessage}. ${details}. ${advice}`;
102
+ }
103
+
104
+ function flexError(providerMessage: string, context: FlexErrorContext): Error {
105
+ return new Error(buildFlexErrorMessage(providerMessage, context));
106
+ }
107
+
49
108
  export async function requestFlexStatement(config: FlexQueryConfig): Promise<string> {
50
109
  const endpoint = config.endpoint || IBKR_STATEMENT_URL;
51
110
  const url = `${endpoint}?t=${config.token}&q=${config.queryId}&v=3`;
52
- const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
53
- const text = await resp.text();
111
+ let resp: Response;
112
+ let text: string;
113
+ try {
114
+ resp = await httpFetch(url, { signal: AbortSignal.timeout(30_000) });
115
+ text = await resp.text();
116
+ } catch (error) {
117
+ throw flexError(error instanceof Error ? error.message : "Network request failed", {
118
+ phase: "request",
119
+ endpoint,
120
+ queryId: config.queryId,
121
+ token: config.token,
122
+ });
123
+ }
54
124
 
55
125
  const codeMatch = text.match(/<ReferenceCode>(\d+)<\/ReferenceCode>/);
56
126
  if (!codeMatch) {
57
- const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
58
- throw new Error(errorMatch?.[1] || "Failed to request Flex statement");
127
+ throw flexError(extractFlexErrorMessage(text) || "No reference code returned", {
128
+ phase: "request",
129
+ endpoint,
130
+ queryId: config.queryId,
131
+ token: config.token,
132
+ httpStatus: resp.status,
133
+ });
59
134
  }
60
135
 
61
136
  return codeMatch[1]!;
62
137
  }
63
138
 
64
- export async function getFlexStatement(token: string, referenceCode: string): Promise<string> {
139
+ export async function getFlexStatement(
140
+ token: string,
141
+ referenceCode: string,
142
+ context: Pick<FlexQueryConfig, "endpoint" | "queryId"> = {},
143
+ ): Promise<string> {
65
144
  await new Promise((resolve) => setTimeout(resolve, 3_000));
66
145
 
67
146
  const url = `${IBKR_STATEMENT_GET_URL}?t=${token}&q=${referenceCode}&v=3`;
68
147
  for (let attempt = 0; attempt < 5; attempt += 1) {
69
- const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
70
- const text = await resp.text();
148
+ let resp: Response;
149
+ let text: string;
150
+ try {
151
+ resp = await httpFetch(url, { signal: AbortSignal.timeout(30_000) });
152
+ text = await resp.text();
153
+ } catch (error) {
154
+ throw flexError(error instanceof Error ? error.message : "Network request failed", {
155
+ phase: "download",
156
+ endpoint: context.endpoint,
157
+ queryId: context.queryId,
158
+ token,
159
+ referenceCode,
160
+ });
161
+ }
71
162
 
72
163
  if (text.includes("<FlexQueryResponse") || text.includes("<FlexStatements")) {
73
164
  return text;
@@ -78,13 +169,26 @@ export async function getFlexStatement(token: string, referenceCode: string): Pr
78
169
  continue;
79
170
  }
80
171
 
81
- const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
82
- if (errorMatch) {
83
- throw new Error(errorMatch[1]);
172
+ const providerMessage = extractFlexErrorMessage(text);
173
+ if (providerMessage) {
174
+ throw flexError(providerMessage, {
175
+ phase: "download",
176
+ endpoint: context.endpoint,
177
+ queryId: context.queryId,
178
+ token,
179
+ referenceCode,
180
+ httpStatus: resp.status,
181
+ });
84
182
  }
85
183
  }
86
184
 
87
- throw new Error("Flex statement generation timed out");
185
+ throw flexError("statement generation timed out", {
186
+ phase: "download",
187
+ endpoint: context.endpoint,
188
+ queryId: context.queryId,
189
+ token,
190
+ referenceCode,
191
+ });
88
192
  }
89
193
 
90
194
  export async function loadFlexStatement(config: FlexQueryConfig): Promise<string> {
@@ -96,7 +200,7 @@ export async function loadFlexStatement(config: FlexQueryConfig): Promise<string
96
200
 
97
201
  const promise = (async () => {
98
202
  const referenceCode = await requestFlexStatement(config);
99
- return getFlexStatement(config.token, referenceCode);
203
+ return getFlexStatement(config.token, referenceCode, config);
100
204
  })();
101
205
  flexStatementCache.set(cacheKey, { createdAt: Date.now(), promise });
102
206
  setTimeout(() => {