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
@@ -1,7 +1,8 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
3
- import { useKeyboard } from "@opentui/react";
4
- import { DataTable, TabBar, type DataTableColumn } from "../../../components";
1
+ import { Box, Text } from "../../../ui";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { TextAttributes } from "../../../ui";
4
+ import { useShortcut } from "../../../react/input";
5
+ import { DataTableView, Tabs, type DataTableColumn } from "../../../components";
5
6
  import type { GloomPlugin, PaneProps } from "../../../types/plugin";
6
7
  import type { ColumnConfig } from "../../../types/config";
7
8
  import type { TickerFinancials } from "../../../types/financials";
@@ -14,7 +15,6 @@ import {
14
15
  usePaneInstance,
15
16
  usePaneStateValue,
16
17
  } from "../../../state/app-context";
17
- import { getCollectionTickers } from "../../../state/selectors";
18
18
  import { useChartQueries, useFxRatesMap, useTickerFinancialsMap } from "../../../market-data/hooks";
19
19
  import { instrumentFromTicker, type ChartRequest } from "../../../market-data/request-types";
20
20
  import { buildChartKey } from "../../../market-data/selectors";
@@ -250,10 +250,15 @@ function nextSectorSortPreference(current: SectorSortPreference, columnId: strin
250
250
  }
251
251
 
252
252
  export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
253
- const state = useAppSelector((s) => s);
253
+ const focusedCollectionId = useAppSelector((state) => getFocusedCollectionId(state));
254
+ const portfolios = useAppSelector((state) => state.config.portfolios);
255
+ const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
256
+ const tickersBySymbol = useAppSelector((state) => state.tickers);
257
+ const cachedFinancials = useAppSelector((state) => state.financials);
258
+ const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
259
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
260
+ const config = useAppSelector((state) => state.config);
254
261
  const paneInstance = usePaneInstance();
255
- const focusedCollectionId = getFocusedCollectionId(state);
256
- const portfolios = state.config.portfolios;
257
262
  const requestedPortfolioId = paneInstance?.params?.portfolioId ?? paneInstance?.params?.collectionId;
258
263
  const fallbackPortfolioId = useMemo(
259
264
  () => (
@@ -266,12 +271,8 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
266
271
 
267
272
  const [currentPortfolioId, setCurrentPortfolioId] = usePaneStateValue<string>("portfolioId", fallbackPortfolioId);
268
273
  const [selectedSectorId, setSelectedSectorId] = useState<string | null>(null);
269
- const [hoveredSectorIdx, setHoveredSectorIdx] = useState<number | null>(null);
270
274
  const [sectorSort, setSectorSort] = useState<SectorSortPreference>(DEFAULT_SECTOR_SORT);
271
275
 
272
- const sectorScrollRef = useRef<ScrollBoxRenderable>(null);
273
- const sectorHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
274
-
275
276
  const activePortfolioId = resolvePortfolioId(portfolios, currentPortfolioId) ?? fallbackPortfolioId;
276
277
  const activePortfolio = useMemo(
277
278
  () => portfolios.find((portfolio) => portfolio.id === activePortfolioId) ?? null,
@@ -286,15 +287,14 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
286
287
  const handlePortfolioSelect = useCallback((portfolioId: string) => {
287
288
  setCurrentPortfolioId(portfolioId);
288
289
  setSelectedSectorId(null);
289
- setHoveredSectorIdx(null);
290
- sectorScrollRef.current?.scrollTo(0);
291
290
  }, [setCurrentPortfolioId]);
292
291
 
293
292
  const portfolioTickers = useMemo(() => {
294
293
  if (!activePortfolioId) return [];
295
- return getCollectionTickers(state, activePortfolioId)
294
+ return [...tickersBySymbol.values()]
295
+ .filter((ticker) => ticker.metadata.portfolios.includes(activePortfolioId))
296
296
  .filter((ticker) => hasPortfolioPosition(ticker, activePortfolioId));
297
- }, [activePortfolioId, state]);
297
+ }, [activePortfolioId, tickersBySymbol]);
298
298
 
299
299
  const chartTargets = useMemo<PortfolioChartTarget[]>(
300
300
  () => portfolioTickers.flatMap((ticker) => {
@@ -330,36 +330,37 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
330
330
 
331
331
  const marketFinancials = useTickerFinancialsMap(portfolioTickers);
332
332
  const financials = useMemo(() => {
333
- const merged = new Map(state.financials);
333
+ const merged = new Map(cachedFinancials);
334
334
  for (const [symbol, data] of marketFinancials) {
335
335
  merged.set(symbol, data);
336
336
  }
337
337
  return merged;
338
- }, [marketFinancials, state.financials]);
339
- const accountState = usePortfolioAccountState(activePortfolio, state);
338
+ }, [cachedFinancials, marketFinancials]);
339
+ const accountStateInput = useMemo(() => ({ brokerAccounts, config }), [brokerAccounts, config]);
340
+ const accountState = usePortfolioAccountState(activePortfolio, accountStateInput);
340
341
  const trackedCurrencies = useMemo(
341
- () => buildTrackedCurrencies(portfolioTickers, financials, state.config.baseCurrency),
342
- [financials, portfolioTickers, state.config.baseCurrency],
342
+ () => buildTrackedCurrencies(portfolioTickers, financials, baseCurrency),
343
+ [baseCurrency, financials, portfolioTickers],
343
344
  );
344
345
  const fetchedExchangeRates = useFxRatesMap(trackedCurrencies);
345
- const effectiveExchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, state.exchangeRates);
346
+ const effectiveExchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, cachedExchangeRates);
346
347
  const columnContext = useMemo<ColumnContext>(() => ({
347
348
  activeTab: activePortfolioId || undefined,
348
- baseCurrency: state.config.baseCurrency,
349
+ baseCurrency,
349
350
  exchangeRates: effectiveExchangeRates,
350
351
  now: Date.now(),
351
- }), [activePortfolioId, effectiveExchangeRates, state.config.baseCurrency]);
352
+ }), [activePortfolioId, baseCurrency, effectiveExchangeRates]);
352
353
 
353
354
  const portfolioStats = useMemo(
354
355
  () => calculatePortfolioSummaryTotals(
355
356
  portfolioTickers,
356
357
  financials,
357
- state.config.baseCurrency,
358
+ baseCurrency,
358
359
  effectiveExchangeRates,
359
360
  true,
360
361
  activePortfolioId || null,
361
362
  ),
362
- [activePortfolioId, effectiveExchangeRates, financials, portfolioTickers, state.config.baseCurrency],
363
+ [activePortfolioId, baseCurrency, effectiveExchangeRates, financials, portfolioTickers],
363
364
  );
364
365
 
365
366
  const portfolioReturnSeries = useMemo(() => {
@@ -550,23 +551,11 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
550
551
  ], [beta, sharpe]);
551
552
  const metricsHeight = summaryRows.length + riskRows.length + 5;
552
553
 
553
- const syncSectorHeaderScroll = useCallback(() => {
554
- const bodyScrollBox = sectorScrollRef.current;
555
- const headerScrollBox = sectorHeaderScrollRef.current;
556
- if (bodyScrollBox && headerScrollBox && headerScrollBox.scrollLeft !== bodyScrollBox.scrollLeft) {
557
- headerScrollBox.scrollLeft = bodyScrollBox.scrollLeft;
558
- }
559
- }, []);
560
-
561
- const handleSectorBodyScrollActivity = useCallback(() => {
562
- syncSectorHeaderScroll();
563
- }, [syncSectorHeaderScroll]);
564
-
565
554
  const handleSectorHeaderClick = useCallback((columnId: string) => {
566
555
  setSectorSort((current) => nextSectorSortPreference(current, columnId));
567
556
  }, []);
568
557
 
569
- useKeyboard((event) => {
558
+ useShortcut((event) => {
570
559
  if (!focused) return;
571
560
 
572
561
  const key = event.name;
@@ -580,15 +569,6 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
580
569
  if (nextPortfolio) handlePortfolioSelect(nextPortfolio.id);
581
570
  return;
582
571
  }
583
- if (key === "j" || key === "down") {
584
- const nextRow = sortedSectorRows[Math.min(safeSelectedSectorIdx + 1, sortedSectorRows.length - 1)];
585
- if (nextRow) setSelectedSectorId(nextRow.id);
586
- return;
587
- }
588
- if (key === "k" || key === "up") {
589
- const nextRow = sortedSectorRows[Math.max(safeSelectedSectorIdx - 1, 0)];
590
- if (nextRow) setSelectedSectorId(nextRow.id);
591
- }
592
572
  });
593
573
 
594
574
  useEffect(() => {
@@ -597,108 +577,86 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
597
577
  }
598
578
  }, [activePortfolioId, currentPortfolioId, setCurrentPortfolioId]);
599
579
 
600
- useEffect(() => {
601
- if (sectorHeaderScrollRef.current) {
602
- sectorHeaderScrollRef.current.horizontalScrollBar.visible = false;
603
- }
604
- syncSectorHeaderScroll();
605
- }, [syncSectorHeaderScroll]);
606
-
607
- useEffect(() => {
608
- const scrollBox = sectorScrollRef.current;
609
- if (!scrollBox || selectedSectorIdx < 0) return;
610
-
611
- const viewportHeight = scrollBox.viewport.height;
612
- if (selectedSectorIdx < scrollBox.scrollTop) {
613
- scrollBox.scrollTo(selectedSectorIdx);
614
- } else if (selectedSectorIdx >= scrollBox.scrollTop + viewportHeight) {
615
- scrollBox.scrollTo(selectedSectorIdx - viewportHeight + 1);
616
- }
617
- }, [selectedSectorIdx]);
618
-
619
580
  return (
620
- <box flexDirection="column" width={width} height={height}>
581
+ <Box flexDirection="column" width={width} height={height}>
621
582
  {portfolioTabs.length === 0 ? (
622
- <box paddingX={1} paddingY={1}>
623
- <text fg={colors.textMuted}>No portfolios found</text>
624
- </box>
583
+ <Box paddingX={1} paddingY={1}>
584
+ <Text fg={colors.textMuted}>No portfolios found</Text>
585
+ </Box>
625
586
  ) : (
626
587
  <>
627
- <box flexDirection="row" height={1}>
628
- <box flexShrink={1} overflow="hidden">
629
- <TabBar
588
+ <Box flexDirection="row" height={1}>
589
+ <Box flexShrink={1} overflow="hidden">
590
+ <Tabs
630
591
  tabs={portfolioTabs}
631
592
  activeValue={activePortfolioId}
632
593
  onSelect={handlePortfolioSelect}
633
594
  compact
634
595
  />
635
- </box>
636
- </box>
596
+ </Box>
597
+ </Box>
637
598
 
638
599
  {!hasPositions ? (
639
- <box paddingX={1} paddingY={1}>
640
- <text fg={colors.textMuted}>
600
+ <Box paddingX={1} paddingY={1}>
601
+ <Text fg={colors.textMuted}>
641
602
  No positions found for {activePortfolio?.name ?? "this portfolio"}
642
- </text>
643
- </box>
603
+ </Text>
604
+ </Box>
644
605
  ) : (
645
606
  <>
646
- <box flexDirection="column" height={metricsHeight} paddingX={1} paddingTop={1}>
647
- <box height={1}>
648
- <text fg={colors.textDim} attributes={TextAttributes.BOLD}>
607
+ <Box flexDirection="column" height={metricsHeight} paddingX={1} paddingTop={1}>
608
+ <Box height={1}>
609
+ <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>
649
610
  Summary
650
- </text>
651
- </box>
611
+ </Text>
612
+ </Box>
652
613
  {summaryRows.map((row) => (
653
- <box key={row.id} flexDirection="row" height={1}>
654
- <box width={14} flexShrink={0}>
655
- <text fg={colors.textDim}>{row.label}</text>
656
- </box>
657
- <text fg={row.color ?? colors.text} attributes={TextAttributes.BOLD}>
614
+ <Box key={row.id} flexDirection="row" height={1}>
615
+ <Box width={14} flexShrink={0}>
616
+ <Text fg={colors.textDim}>{row.label}</Text>
617
+ </Box>
618
+ <Text fg={row.color ?? colors.text} attributes={TextAttributes.BOLD}>
658
619
  {row.value}
659
- </text>
660
- {row.detail && <text fg={colors.textDim}>{` ${row.detail}`}</text>}
661
- </box>
620
+ </Text>
621
+ {row.detail && <Text fg={colors.textDim}>{` ${row.detail}`}</Text>}
622
+ </Box>
662
623
  ))}
663
624
 
664
- <box height={1} />
665
- <box height={1}>
666
- <text fg={colors.textDim} attributes={TextAttributes.BOLD}>
625
+ <Box height={1} />
626
+ <Box height={1}>
627
+ <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>
667
628
  Risk / Return
668
- </text>
669
- </box>
629
+ </Text>
630
+ </Box>
670
631
  {riskRows.map((row) => (
671
- <box key={row.id} flexDirection="row" height={1}>
672
- <box width={14} flexShrink={0}>
673
- <text fg={colors.textDim}>{row.label}</text>
674
- </box>
675
- <text fg={row.color ?? colors.text} attributes={TextAttributes.BOLD}>
632
+ <Box key={row.id} flexDirection="row" height={1}>
633
+ <Box width={14} flexShrink={0}>
634
+ <Text fg={colors.textDim}>{row.label}</Text>
635
+ </Box>
636
+ <Text fg={row.color ?? colors.text} attributes={TextAttributes.BOLD}>
676
637
  {row.value}
677
- </text>
678
- {row.detail && <text fg={colors.textDim}>{` ${row.detail}`}</text>}
679
- </box>
638
+ </Text>
639
+ {row.detail && <Text fg={colors.textDim}>{` ${row.detail}`}</Text>}
640
+ </Box>
680
641
  ))}
681
- <box height={1} />
682
- </box>
642
+ <Box height={1} />
643
+ </Box>
683
644
 
684
- <box height={1} paddingX={1}>
685
- <text fg={colors.textDim} attributes={TextAttributes.BOLD}>
645
+ <Box height={1} paddingX={1}>
646
+ <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>
686
647
  Sector Allocation
687
- </text>
688
- </box>
648
+ </Text>
649
+ </Box>
689
650
 
690
- <DataTable<SectorTableRow, SectorTableColumn>
651
+ <DataTableView<SectorTableRow, SectorTableColumn>
652
+ focused={focused}
653
+ selectedIndex={safeSelectedSectorIdx}
654
+ resetScrollKey={activePortfolioId}
691
655
  columns={sectorColumns}
692
656
  items={sortedSectorRows}
693
657
  sortColumnId={sectorSort.columnId}
694
658
  sortDirection={sectorSort.direction}
695
659
  onHeaderClick={handleSectorHeaderClick}
696
- headerScrollRef={sectorHeaderScrollRef}
697
- scrollRef={sectorScrollRef}
698
- syncHeaderScroll={syncSectorHeaderScroll}
699
- onBodyScrollActivity={handleSectorBodyScrollActivity}
700
- hoveredIdx={hoveredSectorIdx}
701
- setHoveredIdx={setHoveredSectorIdx}
702
660
  getItemKey={(row) => row.id}
703
661
  isSelected={(row) => effectiveSelectedSectorId === row.id}
704
662
  onSelect={(row) => setSelectedSectorId(row.id)}
@@ -734,7 +692,7 @@ export function PortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
734
692
  )}
735
693
  </>
736
694
  )}
737
- </box>
695
+ </Box>
738
696
  );
739
697
  }
740
698
 
@@ -114,10 +114,6 @@ describe("computeSectorAllocation", () => {
114
114
  expect(alloc[0]!.weight).toBeCloseTo(0.5, 2);
115
115
  });
116
116
 
117
- test("returns empty for zero total value", () => {
118
- expect(computeSectorAllocation([])).toEqual([]);
119
- });
120
-
121
117
  test("uses Unknown for missing sector", () => {
122
118
  const alloc = computeSectorAllocation([{ sector: "", marketValue: 100 }]);
123
119
  expect(alloc[0]!.sector).toBe("Unknown");
@@ -1,54 +1,26 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act } from "react";
3
- import { testRender } from "@opentui/react/test-utils";
3
+ import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
4
+ import { testRender } from "../../renderers/opentui/test-utils";
4
5
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
6
+ import { createStatefulTestPluginRuntime } from "../../test-support/plugin-runtime";
5
7
  import { createDefaultConfig } from "../../types/config";
6
- import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
7
- import { setSharedDataProviderForTests, setSharedRegistryForTests } from "../registry";
8
- import { AskAiTab, __resetAskAiHistoryForTests, __setAskAiHistoryForTests, __setDetectedProvidersForTests, type AiProvider } from "./ask-ai";
8
+ import { Box } from "../../ui";
9
+ import { PluginRenderProvider } from "../plugin-runtime";
10
+ import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
11
+ import {
12
+ AskAiDetailTab as AskAiTab,
13
+ __resetAskAiHistoryForTests,
14
+ __setAskAiHistoryForTests,
15
+ __setDetectedProvidersForTests,
16
+ type AiProvider,
17
+ } from "./ai/ask-ai-detail-tab";
9
18
  import type { TickerRecord } from "../../types/ticker";
10
19
 
11
20
  const PANE_ID = "ticker-detail:main";
12
21
 
13
22
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
14
23
 
15
- function makeRuntime(): PluginRuntimeAccess {
16
- const resumeState = new Map<string, unknown>();
17
- const listeners = new Map<string, Set<() => void>>();
18
-
19
- return {
20
- pinTicker() {},
21
- navigateTicker() {},
22
- subscribeResumeState(pluginId, key, listener) {
23
- const storeKey = `${pluginId}:${key}`;
24
- if (!listeners.has(storeKey)) listeners.set(storeKey, new Set());
25
- listeners.get(storeKey)!.add(listener);
26
- return () => listeners.get(storeKey)?.delete(listener);
27
- },
28
- getResumeState(pluginId, key) {
29
- return (resumeState.get(`${pluginId}:${key}`) as any) ?? null;
30
- },
31
- setResumeState(pluginId, key, value) {
32
- const storeKey = `${pluginId}:${key}`;
33
- resumeState.set(storeKey, value);
34
- for (const listener of listeners.get(storeKey) ?? []) listener();
35
- },
36
- deleteResumeState(pluginId, key) {
37
- const storeKey = `${pluginId}:${key}`;
38
- resumeState.delete(storeKey);
39
- for (const listener of listeners.get(storeKey) ?? []) listener();
40
- },
41
- getConfigState() {
42
- return null;
43
- },
44
- async setConfigState() {},
45
- async deleteConfigState() {},
46
- getConfigStateKeys() {
47
- return [];
48
- },
49
- };
50
- }
51
-
52
24
  function makeTicker(symbol: string, name = symbol): TickerRecord {
53
25
  return {
54
26
  metadata: {
@@ -97,8 +69,15 @@ function createAskAiHarness(
97
69
  return (
98
70
  <AppContext value={{ state, dispatch: () => {} }}>
99
71
  <PaneInstanceProvider paneId={PANE_ID}>
100
- <PluginRenderProvider pluginId="ai" runtime={makeRuntime()}>
101
- <AskAiTab width={width} height={height} focused onCapture={onCapture} />
72
+ <PluginRenderProvider pluginId="ai" runtime={createStatefulTestPluginRuntime()}>
73
+ <PaneFooterProvider>
74
+ {(footer) => (
75
+ <Box flexDirection="column" width={width} height={height}>
76
+ <AskAiTab width={width} height={Math.max(1, height - 1)} focused onCapture={onCapture} />
77
+ <PaneFooterBar footer={footer} focused width={width} />
78
+ </Box>
79
+ )}
80
+ </PaneFooterProvider>
102
81
  </PluginRenderProvider>
103
82
  </PaneInstanceProvider>
104
83
  </AppContext>
@@ -120,7 +99,7 @@ afterEach(() => {
120
99
  __setDetectedProvidersForTests(null);
121
100
  __resetAskAiHistoryForTests();
122
101
  setSharedRegistryForTests(undefined);
123
- setSharedDataProviderForTests(undefined);
102
+ setSharedMarketDataForTests(undefined);
124
103
  if (testSetup) {
125
104
  testSetup.renderer.destroy();
126
105
  testSetup = undefined;
@@ -147,7 +126,7 @@ describe("AskAiTab", () => {
147
126
  expect(frame).toContain("codex");
148
127
  });
149
128
 
150
- test("keeps the provider header readable in a narrow pane", async () => {
129
+ test("keeps provider metadata and hints readable in a narrow pane", async () => {
151
130
  setProviders([
152
131
  { id: "claude", name: "Claude", command: "claude", available: true, buildArgs: () => [] },
153
132
  { id: "gemini", name: "Gemini", command: "gemini", available: true, buildArgs: () => [] },
@@ -155,17 +134,39 @@ describe("AskAiTab", () => {
155
134
 
156
135
  await act(async () => {
157
136
  testSetup = await testRender(
158
- createAskAiHarness(24, 12),
159
- { width: 24, height: 12 },
137
+ createAskAiHarness(46, 12),
138
+ { width: 46, height: 12 },
139
+ );
140
+ });
141
+
142
+ await flushFrame();
143
+
144
+ const frame = testSetup.captureCharFrame();
145
+ expect(frame).toContain("Provider Claude");
146
+ expect(frame).toContain("[t]provider");
147
+ });
148
+
149
+ test("keeps the terminal composer clear of the pane footer", async () => {
150
+ setProviders([
151
+ { id: "claude", name: "Claude", command: "claude", available: true, buildArgs: () => [] },
152
+ ]);
153
+
154
+ await act(async () => {
155
+ testSetup = await testRender(
156
+ createAskAiHarness(80, 8),
157
+ { width: 80, height: 8 },
160
158
  );
161
159
  });
162
160
 
163
161
  await flushFrame();
164
162
 
165
- const headerLine = testSetup.captureCharFrame().split("\n").find((line) => line.includes("Ask AI")) ?? "";
166
- expect(headerLine).toContain("Ask AI");
167
- expect(headerLine).toContain("Claude (t)");
168
- expect(headerLine).not.toContain("t to switch");
163
+ const lines = testSetup.captureCharFrame().split("\n");
164
+ const inputRow = lines.findIndex((line) => line.includes("Ask a question..."));
165
+ const footerRow = lines.findIndex((line) => line.includes("Provider Claude"));
166
+
167
+ expect(inputRow).toBeGreaterThanOrEqual(0);
168
+ expect(footerRow).toBeGreaterThan(inputRow + 1);
169
+ expect(lines[inputRow + 1]?.trim()).toBe("");
169
170
  });
170
171
 
171
172
  test("focuses the input when the prompt row is clicked", async () => {
@@ -183,8 +184,8 @@ describe("AskAiTab", () => {
183
184
  await flushFrame();
184
185
 
185
186
  const frameBeforeClick = testSetup.captureCharFrame().split("\n");
186
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Enter to start typing"));
187
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Enter to start typing") ?? -1;
187
+ const inputRow = frameBeforeClick.findIndex((line) => line.includes("Ask a question..."));
188
+ const inputCol = frameBeforeClick[inputRow]?.indexOf("Ask a question...") ?? -1;
188
189
 
189
190
  expect(inputRow).toBeGreaterThanOrEqual(0);
190
191
  expect(inputCol).toBeGreaterThanOrEqual(0);
@@ -224,7 +225,7 @@ describe("AskAiTab", () => {
224
225
 
225
226
  const opened: string[] = [];
226
227
  setSharedRegistryForTests({
227
- pinTickerFn(symbol: string) {
228
+ pinTicker(symbol: string) {
228
229
  opened.push(symbol);
229
230
  },
230
231
  } as any);
@@ -0,0 +1,168 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { Box } from "../../../ui";
4
+ import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane-footer";
5
+ import { testRender } from "../../../renderers/opentui/test-utils";
6
+ import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app-context";
7
+ import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
8
+ import { createDefaultConfig, type BrokerInstanceConfig } from "../../../types/config";
9
+ import { PluginRenderProvider } from "../../plugin-runtime";
10
+ import { ibkrBroker } from "../../ibkr/broker-adapter";
11
+ import { BrokersPane } from "./index";
12
+
13
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
14
+
15
+ afterEach(async () => {
16
+ if (testSetup) {
17
+ await act(async () => {
18
+ testSetup!.renderer.destroy();
19
+ });
20
+ testSetup = undefined;
21
+ }
22
+ });
23
+
24
+ function createGatewayInstance(): BrokerInstanceConfig {
25
+ return {
26
+ id: "ibkr-paper",
27
+ brokerType: "ibkr",
28
+ label: "IBKR Paper",
29
+ connectionMode: "gateway",
30
+ config: {
31
+ connectionMode: "gateway",
32
+ gatewaySetupMode: "manual",
33
+ flex: { token: "", queryId: "", endpoint: "" },
34
+ gateway: { host: "127.0.0.1", port: 4002, marketDataType: "auto" },
35
+ },
36
+ enabled: true,
37
+ };
38
+ }
39
+
40
+ function Harness({
41
+ instance,
42
+ calls,
43
+ paneHeight = 24,
44
+ }: {
45
+ instance?: BrokerInstanceConfig;
46
+ calls: string[];
47
+ paneHeight?: number;
48
+ }) {
49
+ const config = {
50
+ ...createDefaultConfig("/tmp/gloomberb-broker-manager-pane"),
51
+ brokerInstances: instance ? [instance] : [],
52
+ };
53
+ const state = createInitialState(config);
54
+ if (instance) {
55
+ state.brokerAccounts = {
56
+ [instance.id]: [{
57
+ accountId: "DU12345",
58
+ name: "DU12345",
59
+ currency: "USD",
60
+ netLiquidation: 125000,
61
+ buyingPower: 50000,
62
+ }],
63
+ };
64
+ }
65
+ const runtime = createTestPluginRuntime({
66
+ getBrokerAdapter: (brokerType) => brokerType === "ibkr" ? ibkrBroker : null,
67
+ openCommandBar: (query) => calls.push(`command:${query ?? ""}`),
68
+ showWidget: (paneId) => calls.push(`widget:${paneId}`),
69
+ connectBrokerInstance: async (instanceId) => { calls.push(`connect:${instanceId}`); },
70
+ syncBrokerInstance: async (instanceId) => { calls.push(`sync:${instanceId}`); },
71
+ updateBrokerInstance: async (instanceId) => { calls.push(`update:${instanceId}`); },
72
+ });
73
+
74
+ return (
75
+ <AppContext value={{ state, dispatch: () => {} }}>
76
+ <PluginRenderProvider pluginId="broker-manager" runtime={runtime}>
77
+ <PaneInstanceProvider paneId="brokers:test">
78
+ <BrokersPane focused width={92} height={paneHeight} />
79
+ </PaneInstanceProvider>
80
+ </PluginRenderProvider>
81
+ </AppContext>
82
+ );
83
+ }
84
+
85
+ function FooterHarness({
86
+ height = 25,
87
+ ...props
88
+ }: {
89
+ instance?: BrokerInstanceConfig;
90
+ calls: string[];
91
+ height?: number;
92
+ }) {
93
+ return (
94
+ <PaneFooterProvider>
95
+ {(footer) => (
96
+ <Box width={92} height={height} flexDirection="column">
97
+ <Harness {...props} paneHeight={height - 1} />
98
+ <PaneFooterBar footer={footer} focused width={92} />
99
+ </Box>
100
+ )}
101
+ </PaneFooterProvider>
102
+ );
103
+ }
104
+
105
+ async function pressKey(key: string) {
106
+ await act(async () => {
107
+ testSetup!.mockInput.pressKey(key);
108
+ await Promise.resolve();
109
+ await testSetup!.renderOnce();
110
+ await testSetup!.renderOnce();
111
+ });
112
+ }
113
+
114
+ describe("BrokersPane", () => {
115
+ test("shows empty state and opens add broker flow", async () => {
116
+ const calls: string[] = [];
117
+ testSetup = await testRender(<Harness calls={calls} />, { width: 92, height: 24 });
118
+ await act(async () => {
119
+ await testSetup!.renderOnce();
120
+ });
121
+
122
+ const frame = testSetup.captureCharFrame();
123
+ expect(frame).toContain("No broker profiles.");
124
+ expect(frame).not.toContain("Press a or click Add Broker.");
125
+ await act(async () => {
126
+ testSetup!.mockInput.pressKey("a");
127
+ await testSetup!.renderOnce();
128
+ });
129
+
130
+ expect(calls).toEqual(["command:Add Broker Account"]);
131
+ });
132
+
133
+ test("hides unavailable footer actions with no broker profile", async () => {
134
+ const calls: string[] = [];
135
+ testSetup = await testRender(<FooterHarness calls={calls} />, { width: 92, height: 25 });
136
+ await act(async () => {
137
+ await testSetup!.renderOnce();
138
+ await testSetup!.renderOnce();
139
+ });
140
+
141
+ const frame = testSetup.captureCharFrame();
142
+ expect(frame).toContain("[a]dd");
143
+ expect(frame).not.toContain("[c]onnect");
144
+ expect(frame).not.toContain("[d]isconnect");
145
+ });
146
+
147
+ test("renders IBKR row and invokes broker actions", async () => {
148
+ const calls: string[] = [];
149
+ testSetup = await testRender(<FooterHarness calls={calls} instance={createGatewayInstance()} height={35} />, { width: 92, height: 35 });
150
+ await act(async () => {
151
+ await testSetup!.renderOnce();
152
+ await testSetup!.renderOnce();
153
+ });
154
+
155
+ let frame = testSetup.captureCharFrame();
156
+ expect(frame).toContain("PROFILE");
157
+ expect(frame).toContain("STATUS");
158
+ expect(frame).toContain("ACCOUNTS");
159
+ expect(frame).toContain("IBKR Paper");
160
+ expect(frame).not.toContain("DU12345");
161
+
162
+ await pressKey("c");
163
+ await pressKey("s");
164
+ await pressKey("o");
165
+
166
+ expect(calls).toEqual(["connect:ibkr-paper", "sync:ibkr-paper", "widget:ibkr-trading"]);
167
+ });
168
+ });