gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -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(() => {
@@ -1,13 +1,18 @@
1
- import { useSyncExternalStore } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
  import type { BrokerInstanceConfig } from "../../types/config";
3
3
  import { getGatewayConfig } from "./config";
4
4
  import { ibkrGatewayManager } from "./gateway-service";
5
5
 
6
6
  export function useGatewaySnapshot(instanceId?: string) {
7
- return useSyncExternalStore(
8
- (listener) => ibkrGatewayManager.subscribe(instanceId, listener),
7
+ const subscribe = useCallback(
8
+ (listener: () => void) => ibkrGatewayManager.subscribe(instanceId, listener),
9
+ [instanceId],
10
+ );
11
+ const getSnapshot = useCallback(
9
12
  () => ibkrGatewayManager.getSnapshot(instanceId),
13
+ [instanceId],
10
14
  );
15
+ return useSyncExternalStore(subscribe, getSnapshot);
11
16
  }
12
17
 
13
18
  export function getGatewayRequiredMessage(instanceCount: number) {