gloomberb 0.5.1 → 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 -14
  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 +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  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 +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  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 +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -0,0 +1,759 @@
1
+ import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ Button,
5
+ DataTableStackView,
6
+ NumberField,
7
+ SegmentedControl,
8
+ TextField,
9
+ usePaneFooter,
10
+ type DataTableCell,
11
+ type DataTableColumn,
12
+ type PaneHint,
13
+ } from "../../../components";
14
+ import {
15
+ buildBrokerProfileConfig,
16
+ createBrokerProfileDraft,
17
+ getVisibleBrokerConfigFields,
18
+ PRESERVED_PASSWORD_HINT,
19
+ validateBrokerProfileValues,
20
+ type BrokerProfileDraft,
21
+ } from "../../../brokers/profile-form";
22
+ import { useShortcut } from "../../../react/input";
23
+ import {
24
+ useAppDispatch,
25
+ useAppSelector,
26
+ } from "../../../state/app-context";
27
+ import { colors } from "../../../theme/colors";
28
+ import type { BrokerAdapter, BrokerConfigField, BrokerProfileAction } from "../../../types/broker";
29
+ import type { BrokerInstanceConfig } from "../../../types/config";
30
+ import type { GloomPlugin, PaneProps } from "../../../types/plugin";
31
+ import type { BrokerAccount } from "../../../types/trading";
32
+ import { DialogFrame } from "../../../components/ui/frame";
33
+ import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
34
+ import { formatCurrency } from "../../../utils/format";
35
+ import { usePluginAppActions, usePluginBrokerActions } from "../../plugin-runtime";
36
+ import {
37
+ buildBrokerProfileRows,
38
+ formatBrokerUpdatedAt,
39
+ type BrokerDisplayState,
40
+ type BrokerProfileRow,
41
+ } from "./model";
42
+
43
+ type BrokerEditKey = "label" | "enabled" | string;
44
+ type BrokerColumnId = "profile" | "status" | "broker" | "mode" | "accounts" | "updated";
45
+ type BrokerColumn = DataTableColumn & { id: BrokerColumnId };
46
+
47
+ function stateColor(state: BrokerDisplayState): string {
48
+ switch (state) {
49
+ case "connected": return colors.positive;
50
+ case "connecting": return "#e5c07b";
51
+ case "error": return colors.negative;
52
+ case "disabled": return colors.textMuted;
53
+ case "unavailable": return colors.negative;
54
+ default: return colors.textDim;
55
+ }
56
+ }
57
+
58
+ function stateGlyph(state: BrokerDisplayState): string {
59
+ switch (state) {
60
+ case "connected": return "*";
61
+ case "connecting": return "~";
62
+ case "error": return "!";
63
+ case "disabled": return "-";
64
+ case "unavailable": return "x";
65
+ default: return "o";
66
+ }
67
+ }
68
+
69
+ function truncate(value: string, width: number): string {
70
+ if (width <= 0) return "";
71
+ return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value;
72
+ }
73
+
74
+ function isBrokerErrorMessage(message: string | null | undefined): boolean {
75
+ const normalized = message?.toLowerCase() ?? "";
76
+ return normalized.includes("failed") || normalized.includes("required");
77
+ }
78
+
79
+ function ConfirmRemoveBrokerDialog({
80
+ resolve,
81
+ row,
82
+ }: PromptContext<boolean> & { row: BrokerProfileRow }) {
83
+ const confirm = useCallback(() => resolve(true), [resolve]);
84
+ const cancel = useCallback(() => resolve(false), [resolve]);
85
+
86
+ useDialogKeyboard((event) => {
87
+ event.stopPropagation();
88
+ if (event.name === "enter" || event.name === "return") confirm();
89
+ if (event.name === "escape") cancel();
90
+ });
91
+
92
+ return (
93
+ <DialogFrame title="Disconnect broker?" footer="Enter disconnect · Esc cancel">
94
+ <Box flexDirection="column" width={58}>
95
+ <Text fg={colors.text}>{`Remove "${row.label}" and imported broker data?`}</Text>
96
+ <Text fg={colors.textDim}>Broker-managed portfolios, positions, and contracts will be removed.</Text>
97
+ <Box height={1} />
98
+ <Box flexDirection="row" gap={1}>
99
+ <Button label="Disconnect" variant="danger" onPress={confirm} />
100
+ <Button label="Back" variant="secondary" onPress={cancel} />
101
+ </Box>
102
+ </Box>
103
+ </DialogFrame>
104
+ );
105
+ }
106
+
107
+ function brokerFieldLabel(field: BrokerConfigField, focused: boolean): string {
108
+ return focused ? `> ${field.label}` : ` ${field.label}`;
109
+ }
110
+
111
+ function BrokerConfigFieldEditor({
112
+ field,
113
+ draft,
114
+ previous,
115
+ adapter,
116
+ focused,
117
+ onFocus,
118
+ onChange,
119
+ onSubmit,
120
+ }: {
121
+ field: BrokerConfigField;
122
+ draft: BrokerProfileDraft;
123
+ previous: BrokerInstanceConfig;
124
+ adapter: BrokerAdapter;
125
+ focused: boolean;
126
+ onFocus: () => void;
127
+ onChange: (key: string, value: string) => void;
128
+ onSubmit: () => void;
129
+ }) {
130
+ const value = draft.values[field.key] ?? "";
131
+ const previousPassword = field.type === "password"
132
+ ? String(((adapter.toConfigValues?.(previous) ?? previous.config)[field.key] ?? "") || "")
133
+ : "";
134
+
135
+ if (field.type === "select") {
136
+ return (
137
+ <Box flexDirection="column" onMouseDown={onFocus}>
138
+ <Text fg={focused ? colors.textBright : colors.textDim} attributes={focused ? TextAttributes.BOLD : 0}>
139
+ {brokerFieldLabel(field, focused)}
140
+ </Text>
141
+ <SegmentedControl
142
+ value={value}
143
+ options={(field.options ?? []).map((option) => ({ label: option.label, value: option.value }))}
144
+ onChange={(nextValue) => onChange(field.key, nextValue)}
145
+ />
146
+ </Box>
147
+ );
148
+ }
149
+
150
+ const Field = field.type === "number" ? NumberField : TextField;
151
+ return (
152
+ <Box onMouseDown={onFocus}>
153
+ <Field
154
+ label={brokerFieldLabel(field, focused)}
155
+ value={value}
156
+ focused={focused}
157
+ width={34}
158
+ type={field.type === "password" ? "password" : "text"}
159
+ placeholder={field.type === "password" && previousPassword ? PRESERVED_PASSWORD_HINT : field.placeholder}
160
+ hint={field.placeholder}
161
+ onChange={(nextValue) => onChange(field.key, nextValue)}
162
+ onSubmit={onSubmit}
163
+ />
164
+ </Box>
165
+ );
166
+ }
167
+
168
+ function accountDetail(account: BrokerAccount): string {
169
+ const parts = [
170
+ account.accountId,
171
+ account.netLiquidation != null ? `${formatCurrency(account.netLiquidation, account.currency || "USD")} net liq` : null,
172
+ account.buyingPower != null ? `${formatCurrency(account.buyingPower, account.currency || "USD")} buying power` : null,
173
+ ];
174
+ return parts.filter(Boolean).join(" · ");
175
+ }
176
+
177
+ function buildBrokerColumns(width: number): BrokerColumn[] {
178
+ const usableWidth = Math.max(48, width - 4);
179
+ const statusWidth = 13;
180
+ const modeWidth = 11;
181
+ const accountWidth = 14;
182
+ const updatedWidth = 9;
183
+ const brokerWidth = usableWidth >= 84 ? 22 : 18;
184
+ const separators = 6;
185
+ const profileWidth = Math.max(
186
+ 16,
187
+ usableWidth - statusWidth - modeWidth - accountWidth - updatedWidth - brokerWidth - separators,
188
+ );
189
+
190
+ return [
191
+ { id: "profile", label: "PROFILE", width: profileWidth, align: "left" },
192
+ { id: "status", label: "STATUS", width: statusWidth, align: "left" },
193
+ { id: "broker", label: "BROKER", width: brokerWidth, align: "left" },
194
+ { id: "mode", label: "MODE", width: modeWidth, align: "left" },
195
+ { id: "accounts", label: "ACCOUNTS", width: accountWidth, align: "right" },
196
+ { id: "updated", label: "UPDATED", width: updatedWidth, align: "right" },
197
+ ];
198
+ }
199
+
200
+ export function BrokersPane({ focused, width, height }: PaneProps) {
201
+ const dispatch = useAppDispatch();
202
+ const config = useAppSelector((state) => state.config);
203
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
204
+ const dialog = useDialog();
205
+ const { openCommandBar, showWidget } = usePluginAppActions();
206
+ const {
207
+ getBrokerAdapter,
208
+ connectBrokerInstance,
209
+ updateBrokerInstance,
210
+ syncBrokerInstance,
211
+ removeBrokerInstance,
212
+ } = usePluginBrokerActions();
213
+ const [selectedIndex, setSelectedIndex] = useState(0);
214
+ const [editDraft, setEditDraft] = useState<BrokerProfileDraft | null>(null);
215
+ const [activeEditKey, setActiveEditKey] = useState<BrokerEditKey>("label");
216
+ const [busy, setBusy] = useState<string | null>(null);
217
+ const [message, setMessage] = useState<string | null>(null);
218
+ const [statusVersion, setStatusVersion] = useState(0);
219
+ const [detailOpen, setDetailOpen] = useState(false);
220
+
221
+ const adapters = useMemo(() => {
222
+ const next = new Map<string, BrokerAdapter | null>();
223
+ for (const instance of config.brokerInstances) {
224
+ if (!next.has(instance.brokerType)) next.set(instance.brokerType, getBrokerAdapter(instance.brokerType));
225
+ }
226
+ return next;
227
+ }, [config.brokerInstances, getBrokerAdapter]);
228
+
229
+ useEffect(() => {
230
+ const disposers = config.brokerInstances.flatMap((instance) => {
231
+ const adapter = getBrokerAdapter(instance.brokerType);
232
+ const dispose = adapter?.subscribeStatus?.(instance, () => setStatusVersion((version) => version + 1));
233
+ return dispose ? [dispose] : [];
234
+ });
235
+ return () => {
236
+ for (const dispose of disposers) dispose();
237
+ };
238
+ }, [config.brokerInstances, getBrokerAdapter]);
239
+
240
+ const rows = useMemo(
241
+ () => buildBrokerProfileRows(config, adapters, brokerAccounts),
242
+ [adapters, brokerAccounts, config, statusVersion],
243
+ );
244
+ const selectedRow = rows[Math.min(selectedIndex, rows.length - 1)] ?? null;
245
+ const selectedAccounts = selectedRow ? brokerAccounts[selectedRow.id] ?? [] : [];
246
+ const editFields = editDraft && selectedRow?.adapter
247
+ ? getVisibleBrokerConfigFields(selectedRow.adapter, editDraft.values)
248
+ : [];
249
+ const editKeys = useMemo<BrokerEditKey[]>(
250
+ () => ["label", "enabled", ...editFields.map((field) => field.key)],
251
+ [editFields],
252
+ );
253
+
254
+ useEffect(() => {
255
+ setSelectedIndex((current) => Math.max(0, Math.min(current, rows.length - 1)));
256
+ }, [rows.length]);
257
+
258
+ useEffect(() => {
259
+ if (rows.length === 0) {
260
+ setDetailOpen(false);
261
+ }
262
+ }, [rows.length]);
263
+
264
+ useEffect(() => {
265
+ if (!editDraft) return;
266
+ if (!editKeys.includes(activeEditKey)) setActiveEditKey(editKeys[0] ?? "label");
267
+ }, [activeEditKey, editDraft, editKeys]);
268
+
269
+ useEffect(() => {
270
+ if (!focused || !editDraft) return;
271
+ dispatch({ type: "SET_INPUT_CAPTURED", captured: true });
272
+ return () => {
273
+ dispatch({ type: "SET_INPUT_CAPTURED", captured: false });
274
+ };
275
+ }, [dispatch, editDraft, focused]);
276
+
277
+ const openAddBroker = useCallback(() => {
278
+ openCommandBar("Add Broker Account");
279
+ }, [openCommandBar]);
280
+
281
+ const startEdit = useCallback(() => {
282
+ if (!selectedRow?.adapter) {
283
+ setMessage("Broker plugin is not available.");
284
+ return;
285
+ }
286
+ const draft = createBrokerProfileDraft(selectedRow.adapter, selectedRow.instance);
287
+ for (const field of selectedRow.adapter.configSchema) {
288
+ if (field.type === "password" && draft.values[field.key]) draft.values[field.key] = "";
289
+ }
290
+ setEditDraft(draft);
291
+ setActiveEditKey("label");
292
+ setMessage(null);
293
+ setDetailOpen(true);
294
+ }, [selectedRow]);
295
+
296
+ const saveEdit = useCallback(async () => {
297
+ if (!selectedRow?.adapter || !editDraft) return;
298
+ const label = editDraft.label.trim();
299
+ if (!label) {
300
+ setMessage("Profile label is required.");
301
+ return;
302
+ }
303
+ const validationError = validateBrokerProfileValues(selectedRow.adapter, editDraft.values, selectedRow.instance);
304
+ if (validationError) {
305
+ setMessage(validationError);
306
+ return;
307
+ }
308
+
309
+ try {
310
+ setBusy("Saving…");
311
+ const nextConfig = buildBrokerProfileConfig(selectedRow.adapter, editDraft.values, selectedRow.instance);
312
+ await updateBrokerInstance(selectedRow.id, nextConfig, {
313
+ label,
314
+ enabled: editDraft.enabled,
315
+ replaceConfig: true,
316
+ });
317
+ setEditDraft(null);
318
+ setMessage(`Saved ${label}.`);
319
+ } catch (error) {
320
+ setMessage(error instanceof Error ? error.message : "Failed to save broker profile.");
321
+ } finally {
322
+ setBusy(null);
323
+ }
324
+ }, [editDraft, selectedRow, updateBrokerInstance]);
325
+
326
+ const connectSelected = useCallback(async () => {
327
+ if (!selectedRow) return;
328
+ try {
329
+ setBusy("Testing…");
330
+ await connectBrokerInstance(selectedRow.id);
331
+ setStatusVersion((version) => version + 1);
332
+ setMessage(`Tested ${selectedRow.label}.`);
333
+ } catch (error) {
334
+ setMessage(error instanceof Error ? error.message : `Failed to test ${selectedRow.label}.`);
335
+ } finally {
336
+ setBusy(null);
337
+ }
338
+ }, [connectBrokerInstance, selectedRow]);
339
+
340
+ const syncSelected = useCallback(async () => {
341
+ if (!selectedRow) return;
342
+ try {
343
+ setBusy("Syncing…");
344
+ await syncBrokerInstance(selectedRow.id);
345
+ setStatusVersion((version) => version + 1);
346
+ setMessage(`Synced ${selectedRow.label}.`);
347
+ } catch (error) {
348
+ setMessage(error instanceof Error ? error.message : `Failed to sync ${selectedRow.label}.`);
349
+ } finally {
350
+ setBusy(null);
351
+ }
352
+ }, [selectedRow, syncBrokerInstance]);
353
+
354
+ const selectedProfileActions = useMemo(
355
+ () => selectedRow?.adapter?.getProfileActions?.(selectedRow.instance) ?? [],
356
+ [selectedRow],
357
+ );
358
+ const primaryProfileAction = selectedProfileActions[0] ?? null;
359
+
360
+ const openProfileAction = useCallback((action: BrokerProfileAction | null = primaryProfileAction) => {
361
+ if (!action) return;
362
+ if (action.disabled) {
363
+ setMessage(action.disabledReason ?? `${action.label} is unavailable for this profile.`);
364
+ return;
365
+ }
366
+ if (action.widgetId) showWidget(action.widgetId);
367
+ }, [primaryProfileAction, showWidget]);
368
+
369
+ const removeSelected = useCallback(async () => {
370
+ if (!selectedRow) return;
371
+ const confirmed = await dialog.prompt<boolean>({
372
+ closeOnClickOutside: true,
373
+ content: (ctx: PromptContext<boolean>) => (
374
+ <ConfirmRemoveBrokerDialog {...ctx} row={selectedRow} />
375
+ ),
376
+ }).catch(() => false);
377
+ if (confirmed !== true) return;
378
+
379
+ try {
380
+ setBusy("Disconnecting…");
381
+ await removeBrokerInstance(selectedRow.id);
382
+ setEditDraft(null);
383
+ setDetailOpen(false);
384
+ setMessage(`Removed ${selectedRow.label}.`);
385
+ } catch (error) {
386
+ setMessage(error instanceof Error ? error.message : `Failed to remove ${selectedRow.label}.`);
387
+ } finally {
388
+ setBusy(null);
389
+ }
390
+ }, [dialog, removeBrokerInstance, selectedRow]);
391
+
392
+ const hasSelectedRow = selectedRow !== null;
393
+ const selectedHasAdapter = !!selectedRow?.adapter;
394
+ const canUseSelectedBroker = selectedHasAdapter && !busy;
395
+ const canOpenSelectedAction = selectedProfileActions.some((action) => !action.disabled && action.widgetId);
396
+ const canRemoveSelected = hasSelectedRow && !busy;
397
+
398
+ const footerActionsRef = useRef({
399
+ openAddBroker,
400
+ startEdit,
401
+ connectSelected,
402
+ syncSelected,
403
+ openProfileAction,
404
+ removeSelected,
405
+ saveEdit,
406
+ });
407
+ footerActionsRef.current = {
408
+ openAddBroker,
409
+ startEdit,
410
+ connectSelected,
411
+ syncSelected,
412
+ openProfileAction,
413
+ removeSelected,
414
+ saveEdit,
415
+ };
416
+
417
+ const footerHints = useMemo<PaneHint[]>(() => {
418
+ if (editDraft) {
419
+ return [
420
+ { id: "save", key: "enter", label: "save", onPress: () => footerActionsRef.current.saveEdit().catch(() => {}) },
421
+ { id: "cancel", key: "esc", label: "cancel", onPress: () => setEditDraft(null) },
422
+ ];
423
+ }
424
+
425
+ const hints: PaneHint[] = [
426
+ { id: "add", key: "a", label: "dd", onPress: () => footerActionsRef.current.openAddBroker() },
427
+ ];
428
+ if (canUseSelectedBroker) {
429
+ hints.push(
430
+ { id: "edit", key: "e", label: "dit", onPress: () => footerActionsRef.current.startEdit() },
431
+ { id: "connect", key: "c", label: "onnect", onPress: () => footerActionsRef.current.connectSelected().catch(() => {}) },
432
+ { id: "sync", key: "s", label: "ync", onPress: () => footerActionsRef.current.syncSelected().catch(() => {}) },
433
+ );
434
+ }
435
+ if (canOpenSelectedAction) {
436
+ hints.push({ id: "open", key: "o", label: "pen", onPress: () => footerActionsRef.current.openProfileAction() });
437
+ }
438
+ if (canRemoveSelected) {
439
+ hints.push({ id: "disconnect", key: "d", label: "isconnect", onPress: () => footerActionsRef.current.removeSelected().catch(() => {}) });
440
+ }
441
+ return hints;
442
+ }, [canOpenSelectedAction, canRemoveSelected, canUseSelectedBroker, editDraft]);
443
+
444
+ usePaneFooter("broker-manager", () => ({
445
+ hints: footerHints,
446
+ }), [footerHints]);
447
+
448
+ useShortcut((event) => {
449
+ if (!focused) return;
450
+
451
+ if (editDraft) {
452
+ if (event.name === "escape") {
453
+ event.stopPropagation();
454
+ setEditDraft(null);
455
+ return;
456
+ }
457
+ if (event.name === "enter" || event.name === "return") {
458
+ event.stopPropagation();
459
+ saveEdit().catch(() => {});
460
+ return;
461
+ }
462
+ if (event.name === "up" || event.name === "k") {
463
+ event.stopPropagation();
464
+ const index = editKeys.indexOf(activeEditKey);
465
+ setActiveEditKey(editKeys[Math.max(0, index - 1)] ?? "label");
466
+ return;
467
+ }
468
+ if (event.name === "down" || event.name === "j" || event.name === "tab") {
469
+ event.stopPropagation();
470
+ const index = editKeys.indexOf(activeEditKey);
471
+ setActiveEditKey(editKeys[Math.min(editKeys.length - 1, index + 1)] ?? "label");
472
+ return;
473
+ }
474
+ return;
475
+ }
476
+
477
+ switch (event.name) {
478
+ case "a":
479
+ openAddBroker();
480
+ break;
481
+ case "enter":
482
+ case "return":
483
+ if (!detailOpen && hasSelectedRow) {
484
+ event.stopPropagation();
485
+ event.preventDefault?.();
486
+ setEditDraft(null);
487
+ setDetailOpen(true);
488
+ }
489
+ break;
490
+ case "e":
491
+ if (canUseSelectedBroker) startEdit();
492
+ break;
493
+ case "c":
494
+ if (canUseSelectedBroker) connectSelected().catch(() => {});
495
+ break;
496
+ case "s":
497
+ if (canUseSelectedBroker) syncSelected().catch(() => {});
498
+ break;
499
+ case "o":
500
+ if (canOpenSelectedAction) openProfileAction();
501
+ break;
502
+ case "d":
503
+ if (canRemoveSelected) removeSelected().catch(() => {});
504
+ break;
505
+ }
506
+ });
507
+
508
+ const connectedCount = rows.filter((row) => row.state === "connected").length;
509
+ const errorCount = rows.filter((row) => row.state === "error" || row.state === "unavailable").length;
510
+ const bodyHeight = Math.max(5, height - 4);
511
+ const tableWidth = Math.max(24, width - 2);
512
+ const columns = useMemo(() => buildBrokerColumns(tableWidth), [tableWidth]);
513
+
514
+ const renderCell = useCallback((
515
+ row: BrokerProfileRow,
516
+ column: BrokerColumn,
517
+ ): DataTableCell => {
518
+ switch (column.id) {
519
+ case "profile":
520
+ return {
521
+ text: row.label,
522
+ color: colors.text,
523
+ attributes: TextAttributes.BOLD,
524
+ };
525
+ case "status":
526
+ return {
527
+ text: `${stateGlyph(row.state)} ${row.stateLabel}`,
528
+ color: stateColor(row.state),
529
+ };
530
+ case "broker":
531
+ return { text: row.brokerName, color: colors.textDim };
532
+ case "mode":
533
+ return { text: row.mode, color: colors.textDim };
534
+ case "accounts":
535
+ return { text: row.accountSummary, color: row.accountCount > 0 ? colors.text : colors.textMuted };
536
+ case "updated":
537
+ return { text: formatBrokerUpdatedAt(row.updatedAt), color: colors.textMuted };
538
+ }
539
+ return { text: "" };
540
+ }, []);
541
+
542
+ const openSelectedDetail = useCallback((index: number, _row: BrokerProfileRow) => {
543
+ setSelectedIndex(index);
544
+ setEditDraft(null);
545
+ setDetailOpen(true);
546
+ }, []);
547
+
548
+ const selectBrokerRow = useCallback((index: number, row: BrokerProfileRow) => {
549
+ setSelectedIndex(index);
550
+ if (selectedRow?.id !== row.id) {
551
+ setEditDraft(null);
552
+ }
553
+ }, [selectedRow?.id]);
554
+
555
+ const updateDraftValue = (key: string, value: string) => {
556
+ setEditDraft((current) => current
557
+ ? { ...current, values: { ...current.values, [key]: value } }
558
+ : current);
559
+ };
560
+
561
+ const detailContentWidth = Math.max(24, tableWidth - 2);
562
+ const detailStatusMessage = selectedRow
563
+ ? isBrokerErrorMessage(message) ? message : selectedRow.message || "No status message."
564
+ : null;
565
+ const detailContent = selectedRow ? (
566
+ <ScrollBox flexGrow={1} scrollY>
567
+ <Box flexDirection="column">
568
+ <Text fg={stateColor(selectedRow.state)} attributes={TextAttributes.BOLD}>
569
+ {truncate(`${selectedRow.label} · ${selectedRow.stateLabel}`, detailContentWidth)}
570
+ </Text>
571
+ <Text fg={colors.textDim}>
572
+ {truncate(`${selectedRow.brokerName} · ${selectedRow.mode} · ${selectedRow.id}`, detailContentWidth)}
573
+ </Text>
574
+ <Text
575
+ fg={isBrokerErrorMessage(detailStatusMessage) ? colors.negative : colors.textDim}
576
+ width={detailContentWidth}
577
+ wrapText
578
+ >
579
+ {detailStatusMessage}
580
+ </Text>
581
+ <Text fg={colors.textMuted}>{`Updated ${formatBrokerUpdatedAt(selectedRow.updatedAt)}`}</Text>
582
+ <Box height={1} />
583
+
584
+ {editDraft && selectedRow.adapter ? (
585
+ <Box flexDirection="column" gap={1}>
586
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Edit Profile</Text>
587
+ <Box onMouseDown={() => setActiveEditKey("label")}>
588
+ <TextField
589
+ label={activeEditKey === "label" ? "> Profile Label" : " Profile Label"}
590
+ value={editDraft.label}
591
+ focused={activeEditKey === "label"}
592
+ width={34}
593
+ onChange={(label) => setEditDraft((current) => current ? { ...current, label } : current)}
594
+ onSubmit={() => saveEdit().catch(() => {})}
595
+ />
596
+ </Box>
597
+ <Box flexDirection="column" onMouseDown={() => setActiveEditKey("enabled")}>
598
+ <Text fg={activeEditKey === "enabled" ? colors.textBright : colors.textDim} attributes={activeEditKey === "enabled" ? TextAttributes.BOLD : 0}>
599
+ {activeEditKey === "enabled" ? "> Enabled" : " Enabled"}
600
+ </Text>
601
+ <SegmentedControl
602
+ value={editDraft.enabled ? "yes" : "no"}
603
+ options={[
604
+ { label: "Enabled", value: "yes" },
605
+ { label: "Disabled", value: "no" },
606
+ ]}
607
+ onChange={(value) => setEditDraft((current) => current ? { ...current, enabled: value === "yes" } : current)}
608
+ />
609
+ </Box>
610
+ {editFields.map((field) => (
611
+ <BrokerConfigFieldEditor
612
+ key={field.key}
613
+ field={field}
614
+ draft={editDraft}
615
+ previous={selectedRow.instance}
616
+ adapter={selectedRow.adapter}
617
+ focused={activeEditKey === field.key}
618
+ onFocus={() => setActiveEditKey(field.key)}
619
+ onChange={updateDraftValue}
620
+ onSubmit={() => saveEdit().catch(() => {})}
621
+ />
622
+ ))}
623
+ <Box flexDirection="row" gap={1}>
624
+ <Button label="Save" variant="primary" onPress={() => saveEdit().catch(() => {})} disabled={!!busy} />
625
+ <Button label="Cancel" variant="secondary" onPress={() => setEditDraft(null)} disabled={!!busy} />
626
+ </Box>
627
+ </Box>
628
+ ) : (
629
+ <Box flexDirection="column">
630
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Accounts</Text>
631
+ {selectedAccounts.length === 0 ? (
632
+ <Text fg={colors.textDim}>No accounts loaded. Test/connect or sync this profile.</Text>
633
+ ) : selectedAccounts.map((account) => (
634
+ <Text key={account.accountId} fg={colors.textDim}>
635
+ {truncate(accountDetail(account), detailContentWidth)}
636
+ </Text>
637
+ ))}
638
+ <Box height={1} />
639
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Actions</Text>
640
+ <Box flexDirection="row" gap={1}>
641
+ <Button label="Edit" onPress={startEdit} disabled={!selectedRow.adapter || !!busy} />
642
+ <Button label="Test" onPress={() => connectSelected().catch(() => {})} disabled={!selectedRow.adapter || !!busy} />
643
+ <Button label="Sync" onPress={() => syncSelected().catch(() => {})} disabled={!selectedRow.adapter || !!busy} />
644
+ </Box>
645
+ <Box height={1} />
646
+ <Box flexDirection="row" gap={1}>
647
+ {selectedProfileActions.map((action) => (
648
+ <Button
649
+ key={action.id}
650
+ label={action.label}
651
+ onPress={() => openProfileAction(action)}
652
+ disabled={!!busy || !!action.disabled}
653
+ />
654
+ ))}
655
+ <Button label="Disconnect" variant="danger" onPress={() => removeSelected().catch(() => {})} disabled={!!busy} />
656
+ </Box>
657
+ </Box>
658
+ )}
659
+ </Box>
660
+ </ScrollBox>
661
+ ) : (
662
+ <Box flexGrow={1} />
663
+ );
664
+
665
+ return (
666
+ <Box flexDirection="column" flexGrow={1} paddingX={1}>
667
+ <Box height={1} flexDirection="row">
668
+ <Box flexGrow={1} flexDirection="row">
669
+ <Box width={8}>
670
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Brokers</Text>
671
+ </Box>
672
+ <Text fg={colors.textDim}>{`${rows.length} profiles · ${connectedCount} connected · ${errorCount} issues`}</Text>
673
+ </Box>
674
+ {busy && <Text fg={colors.textDim}>{busy}</Text>}
675
+ </Box>
676
+ <Box height={1}>
677
+ <Text fg={isBrokerErrorMessage(message) ? colors.negative : colors.textDim}>
678
+ {message || "Manage broker profiles, connection tests, and position syncs."}
679
+ </Text>
680
+ </Box>
681
+ <Box height={1}>
682
+ <Text fg={colors.border}>{"─".repeat(Math.max(1, width - 2))}</Text>
683
+ </Box>
684
+
685
+ <Box height={bodyHeight} overflow="hidden">
686
+ <DataTableStackView<BrokerProfileRow, BrokerColumn>
687
+ focused={focused}
688
+ detailOpen={detailOpen && !!selectedRow}
689
+ onBack={() => {
690
+ setEditDraft(null);
691
+ setDetailOpen(false);
692
+ }}
693
+ detailContent={detailContent}
694
+ detailTitle={selectedRow?.label}
695
+ rootWidth={tableWidth}
696
+ rootHeight={bodyHeight}
697
+ selectedIndex={Math.min(selectedIndex, Math.max(0, rows.length - 1))}
698
+ onSelectIndex={selectBrokerRow}
699
+ onActivateIndex={openSelectedDetail}
700
+ columns={columns}
701
+ items={rows}
702
+ sortColumnId={null}
703
+ sortDirection="asc"
704
+ onHeaderClick={() => {}}
705
+ getItemKey={(row) => row.id}
706
+ isSelected={(row) => selectedRow?.id === row.id}
707
+ onSelect={(row, index) => selectBrokerRow(index, row)}
708
+ onActivate={(row, index) => openSelectedDetail(index, row)}
709
+ renderCell={renderCell}
710
+ emptyStateTitle="No broker profiles."
711
+ emptyStateHint="Add a broker profile to test connections and sync positions."
712
+ showHorizontalScrollbar={false}
713
+ />
714
+ </Box>
715
+ </Box>
716
+ );
717
+ }
718
+
719
+ export const brokerManagerPlugin: GloomPlugin = {
720
+ id: "broker-manager",
721
+ name: "Broker Manager",
722
+ version: "1.0.0",
723
+ description: "Manage broker profiles and connection status",
724
+ panes: [
725
+ {
726
+ id: "brokers",
727
+ name: "Brokers",
728
+ icon: "B",
729
+ component: BrokersPane,
730
+ defaultPosition: "right",
731
+ defaultMode: "floating",
732
+ defaultFloatingSize: { width: 92, height: 24 },
733
+ },
734
+ ],
735
+ paneTemplates: [
736
+ {
737
+ id: "brokers-pane",
738
+ paneId: "brokers",
739
+ label: "Brokers",
740
+ description: "Open broker profiles and connection status",
741
+ keywords: ["broker", "brokers", "connection", "status"],
742
+ shortcut: { prefix: "BR" },
743
+ createInstance: () => ({ placement: "floating" }),
744
+ },
745
+ ],
746
+
747
+ setup(ctx) {
748
+ ctx.registerCommand({
749
+ id: "open-brokers",
750
+ label: "Open Brokers",
751
+ description: "Manage broker profiles and connection status",
752
+ keywords: ["broker", "brokers", "connection", "accounts", "sync"],
753
+ category: "navigation",
754
+ execute: () => {
755
+ ctx.showWidget("brokers");
756
+ },
757
+ });
758
+ },
759
+ };