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,17 +1,22 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useEffect, useRef, useState } from "react";
3
- import { testRender } from "@opentui/react/test-utils";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
4
  import { DialogProvider } from "@opentui-ui/dialog/react";
5
5
  import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
6
6
  import { Button } from "./button";
7
+ import { ChoiceDialog } from "./choice-dialog";
7
8
  import { DataTable } from "./data-table";
8
9
  import { TextField } from "./fields";
9
10
  import { ListView } from "./list-view";
10
- import { MultiSelectChips } from "./multi-select-chips";
11
11
  import { MultiSelectDialogButton } from "./multi-select-dialog";
12
- import { toggleMultiSelectValue } from "./multi-select";
13
- import { ProgressBar } from "./loading";
14
- import { Notice } from "./status";
12
+ import {
13
+ getMultiSelectDisplayValues,
14
+ moveMultiSelectDisplayValue,
15
+ normalizeOrderedMultiSelectValues,
16
+ orderMultiSelectOptionsForDisplay,
17
+ toggleMultiSelectValue,
18
+ toggleOrderedMultiSelectValue,
19
+ } from "./multi-select";
15
20
  import { Tabs } from "./tabs";
16
21
  import { ToggleList } from "../toggle-list";
17
22
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../state/app-context";
@@ -21,8 +26,10 @@ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
21
26
  let setListSelection: ((index: number) => void) | null = null;
22
27
  let selectedTableRow: string | null = null;
23
28
  let activatedTableRow: string | null = null;
24
- let selectedChips: string[] = [];
25
29
  let tableHorizontalScrollbarVisible: boolean | null = null;
30
+ let closedTab: string | null = null;
31
+ let addedTab = false;
32
+ let resolvedChoice: string | null = null;
26
33
 
27
34
  function ScrollableListHarness() {
28
35
  const [selectedIndex, setSelectedIndex] = useState(0);
@@ -150,20 +157,33 @@ function DataTableNoHorizontalScrollHarness() {
150
157
  );
151
158
  }
152
159
 
153
- function MultiSelectChipsHarness() {
154
- const [values, setValues] = useState(["sma"]);
155
- selectedChips = values;
160
+ function DataTableVirtualizationHarness() {
161
+ const headerScrollRef = useRef<ScrollBoxRenderable>(null);
162
+ const scrollRef = useRef<ScrollBoxRenderable>(null);
163
+ const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
164
+ const rows = Array.from({ length: 100 }, (_, index) => ({
165
+ id: `row-${index}`,
166
+ name: `Row ${index}`,
167
+ }));
156
168
 
157
169
  return (
158
- <MultiSelectChips
159
- label="IND"
160
- selectedValues={values}
161
- onChange={setValues}
162
- idPrefix="indicator-chip"
163
- options={[
164
- { value: "sma", label: "SMA" },
165
- { value: "ema", label: "EMA" },
166
- ]}
170
+ <DataTable
171
+ columns={[{ id: "name", label: "NAME", width: 12, align: "left" }]}
172
+ items={rows}
173
+ sortColumnId={null}
174
+ sortDirection="asc"
175
+ onHeaderClick={() => {}}
176
+ headerScrollRef={headerScrollRef}
177
+ scrollRef={scrollRef}
178
+ syncHeaderScroll={() => {}}
179
+ onBodyScrollActivity={() => {}}
180
+ hoveredIdx={hoveredIdx}
181
+ setHoveredIdx={setHoveredIdx}
182
+ getItemKey={(row) => row.id}
183
+ isSelected={() => false}
184
+ onSelect={() => {}}
185
+ renderCell={(row) => ({ text: row.name })}
186
+ emptyStateTitle="No rows."
167
187
  />
168
188
  );
169
189
  }
@@ -188,6 +208,43 @@ function MultiSelectDialogButtonHarness() {
188
208
  );
189
209
  }
190
210
 
211
+ function ChoiceDialogHarness() {
212
+ return (
213
+ <ChoiceDialog
214
+ title="Choose Account"
215
+ dismiss={() => {}}
216
+ resolve={(value) => {
217
+ resolvedChoice = value;
218
+ }}
219
+ choices={[
220
+ { id: "alpha", label: "Alpha", description: "Alpha account" },
221
+ { id: "beta", label: "Beta", description: "Beta account" },
222
+ { id: "gamma", label: "Gamma", description: "Gamma account" },
223
+ ]}
224
+ />
225
+ );
226
+ }
227
+
228
+ async function emitKeypress(event: { name?: string; sequence?: string }) {
229
+ await act(async () => {
230
+ testSetup!.renderer.keyInput.emit("keypress", {
231
+ ctrl: false,
232
+ meta: false,
233
+ option: false,
234
+ shift: false,
235
+ eventType: "press",
236
+ repeated: false,
237
+ preventDefault: () => {},
238
+ stopPropagation: () => {},
239
+ ...event,
240
+ } as any);
241
+ await Promise.resolve();
242
+ await testSetup!.renderOnce();
243
+ await testSetup!.renderOnce();
244
+ });
245
+ await testSetup!.renderOnce();
246
+ }
247
+
191
248
  afterEach(() => {
192
249
  if (testSetup) {
193
250
  testSetup.renderer.destroy();
@@ -196,12 +253,14 @@ afterEach(() => {
196
253
  setListSelection = null;
197
254
  selectedTableRow = null;
198
255
  activatedTableRow = null;
199
- selectedChips = [];
200
256
  tableHorizontalScrollbarVisible = null;
257
+ closedTab = null;
258
+ addedTab = false;
259
+ resolvedChoice = null;
201
260
  });
202
261
 
203
262
  describe("shared UI kit", () => {
204
- test("renders navigation and feedback primitives", async () => {
263
+ test("renders navigation and button primitives", async () => {
205
264
  testSetup = await testRender(
206
265
  <box flexDirection="column">
207
266
  <Tabs
@@ -214,12 +273,8 @@ describe("shared UI kit", () => {
214
273
  />
215
274
  <box height={1} />
216
275
  <Button label="Save" variant="primary" shortcut="⌘S" onPress={() => {}} />
217
- <box height={1} />
218
- <ProgressBar value={0.5} width={8} label="Syncing" />
219
- <box height={1} />
220
- <Notice title="Connected" message="Broker session is live" tone="success" />
221
276
  </box>,
222
- { width: 40, height: 10 },
277
+ { width: 40, height: 6 },
223
278
  );
224
279
 
225
280
  await testSetup.renderOnce();
@@ -227,57 +282,126 @@ describe("shared UI kit", () => {
227
282
  const frame = testSetup.captureCharFrame();
228
283
  expect(frame).toContain("Overview");
229
284
  expect(frame).toContain("Save");
230
- expect(frame).toContain("Syncing");
231
- expect(frame).toContain("Connected");
232
285
  });
233
286
 
234
- test("renders toggle lists with selection and descriptions", async () => {
287
+ test("scrolls overflowing tabs horizontally with the mouse wheel", async () => {
235
288
  testSetup = await testRender(
236
- <ToggleList
237
- items={[
238
- { id: "news", label: "News", enabled: true, description: "Headlines and previews" },
239
- { id: "notes", label: "Notes", enabled: false, description: "Ticker notes" },
289
+ <Tabs
290
+ tabs={[
291
+ { label: "Overview", value: "overview" },
292
+ { label: "Financials", value: "financials" },
293
+ { label: "Chart", value: "chart" },
294
+ { label: "Options", value: "options" },
295
+ { label: "Insider", value: "insider" },
240
296
  ]}
241
- selectedIdx={0}
297
+ activeValue="overview"
242
298
  onSelect={() => {}}
243
- onToggle={() => {}}
244
299
  />,
245
- { width: 40, height: 6 },
300
+ { width: 24, height: 4 },
246
301
  );
247
302
 
248
- await testSetup.renderOnce();
303
+ await act(async () => {
304
+ await testSetup!.renderOnce();
305
+ });
249
306
 
250
- const frame = testSetup.captureCharFrame();
251
- expect(frame).toContain("[✓] News");
252
- expect(frame).toContain("Notes");
253
- expect(frame).toContain("Headlines and previews");
307
+ let frame = testSetup.captureCharFrame();
308
+ expect(frame).toContain("Overview");
309
+ expect(frame).not.toContain("Insider");
310
+
311
+ await act(async () => {
312
+ for (let i = 0; i < 40; i++) {
313
+ await testSetup!.mockMouse.scroll(1, 0, "down");
314
+ }
315
+ await testSetup!.renderOnce();
316
+ });
317
+
318
+ frame = testSetup.captureCharFrame();
319
+ expect(frame).toContain("Options");
320
+ expect(frame).toContain("Insider");
254
321
  });
255
322
 
256
- test("toggles multi-select chips with the mouse", async () => {
257
- testSetup = await testRender(<MultiSelectChipsHarness />, { width: 32, height: 3 });
323
+ test("renders tab actions for editable tab sets", async () => {
324
+ testSetup = await testRender(
325
+ <Tabs
326
+ tabs={[
327
+ { label: "One", value: "one", onClose: (value) => { closedTab = value; } },
328
+ { label: "Two", value: "two" },
329
+ ]}
330
+ activeValue="one"
331
+ onSelect={() => {}}
332
+ compact
333
+ variant="pill"
334
+ closeMode="active"
335
+ onAdd={() => { addedTab = true; }}
336
+ />,
337
+ { width: 24, height: 3 },
338
+ );
258
339
 
259
340
  await act(async () => {
260
341
  await testSetup!.renderOnce();
261
342
  });
262
343
 
263
- let frame = testSetup.captureCharFrame();
264
- expect(frame).toContain("[x] SMA");
265
- expect(frame).toContain("[ ] EMA");
266
- const emaChip = testSetup.renderer.root.findDescendantById("indicator-chip:ema") as BoxRenderable | undefined;
267
- expect(emaChip).toBeDefined();
344
+ const frame = testSetup.captureCharFrame();
345
+ expect(frame).toContain("One x");
346
+ expect(frame).toContain("+");
268
347
 
269
348
  await act(async () => {
270
- await testSetup!.mockMouse.click(emaChip!.x + 1, emaChip!.y);
349
+ await testSetup!.mockMouse.click(5, 0);
350
+ await testSetup!.renderOnce();
271
351
  });
272
- await new Promise((resolve) => setTimeout(resolve, 0));
352
+ expect(closedTab).toBe("one");
353
+
273
354
  await act(async () => {
355
+ await testSetup!.mockMouse.click(14, 0);
274
356
  await testSetup!.renderOnce();
275
357
  });
358
+ expect(addedTab).toBe(true);
359
+ });
276
360
 
277
- frame = testSetup.captureCharFrame();
278
- expect(frame).toContain("[x] SMA");
279
- expect(frame).toContain("[x] EMA");
280
- expect(selectedChips).toEqual(["sma", "ema"]);
361
+ test("renders default tabs in one row", async () => {
362
+ testSetup = await testRender(
363
+ <box flexDirection="column">
364
+ <Tabs
365
+ tabs={[
366
+ { label: "Overview", value: "overview" },
367
+ { label: "Chart", value: "chart" },
368
+ ]}
369
+ activeValue="chart"
370
+ onSelect={() => {}}
371
+ />
372
+ <text>After tabs</text>
373
+ </box>,
374
+ { width: 32, height: 3 },
375
+ );
376
+
377
+ await testSetup.renderOnce();
378
+
379
+ const lines = testSetup.captureCharFrame().split("\n");
380
+ expect(lines[0]).toContain("Overview");
381
+ expect(lines[0]).toContain("Chart");
382
+ expect(lines[1]).toContain("After tabs");
383
+ });
384
+
385
+ test("renders toggle lists with selection and descriptions", async () => {
386
+ testSetup = await testRender(
387
+ <ToggleList
388
+ items={[
389
+ { id: "news", label: "News", enabled: true, description: "Headlines and previews" },
390
+ { id: "notes", label: "Notes", enabled: false, description: "Ticker notes" },
391
+ ]}
392
+ selectedIdx={0}
393
+ onSelect={() => {}}
394
+ onToggle={() => {}}
395
+ />,
396
+ { width: 40, height: 6 },
397
+ );
398
+
399
+ await testSetup.renderOnce();
400
+
401
+ const frame = testSetup.captureCharFrame();
402
+ expect(frame).toContain("[✓] News");
403
+ expect(frame).toContain("Notes");
404
+ expect(frame).toContain("Headlines and previews");
281
405
  });
282
406
 
283
407
  test("opens compact multi-select dialogs from a button", async () => {
@@ -316,6 +440,57 @@ describe("shared UI kit", () => {
316
440
  expect(frame).not.toContain("Chart Indicators");
317
441
  });
318
442
 
443
+ test("supports keyboard and pointer selection in choice dialogs", async () => {
444
+ testSetup = await testRender(<ChoiceDialogHarness />, { width: 44, height: 10 });
445
+
446
+ await act(async () => {
447
+ await testSetup!.renderOnce();
448
+ });
449
+
450
+ let frame = testSetup.captureCharFrame();
451
+ expect(frame).toContain("Alpha account");
452
+
453
+ await emitKeypress({ name: "down" });
454
+ frame = testSetup.captureCharFrame();
455
+ expect(frame).toContain("Beta account");
456
+
457
+ await emitKeypress({ name: "k", sequence: "k" });
458
+ frame = testSetup.captureCharFrame();
459
+ expect(frame).toContain("Alpha account");
460
+
461
+ await emitKeypress({ name: "j", sequence: "j" });
462
+ await emitKeypress({ name: "enter", sequence: "\r" });
463
+ expect(resolvedChoice).toBe("beta");
464
+
465
+ const gammaRow = testSetup.captureCharFrame().split("\n").findIndex((line) => line.includes("Gamma"));
466
+ expect(gammaRow).toBeGreaterThanOrEqual(0);
467
+
468
+ await act(async () => {
469
+ await testSetup!.mockMouse.moveTo(2, gammaRow);
470
+ await testSetup!.renderOnce();
471
+ });
472
+ await testSetup.renderOnce();
473
+ frame = testSetup.captureCharFrame();
474
+ expect(frame).toContain("Gamma account");
475
+
476
+ await act(async () => {
477
+ await testSetup!.mockMouse.click(2, gammaRow);
478
+ await testSetup!.renderOnce();
479
+ });
480
+ expect(resolvedChoice).toBe("gamma");
481
+ });
482
+
483
+ test("cancels choice dialogs with escape", async () => {
484
+ testSetup = await testRender(<ChoiceDialogHarness />, { width: 44, height: 10 });
485
+
486
+ await act(async () => {
487
+ await testSetup!.renderOnce();
488
+ });
489
+ await emitKeypress({ name: "escape", sequence: "\u001b" });
490
+
491
+ expect(resolvedChoice).toBe("");
492
+ });
493
+
319
494
  test("keeps shared multi-select values in option order when toggling", () => {
320
495
  const options = [
321
496
  { value: "sma", label: "SMA" },
@@ -326,6 +501,24 @@ describe("shared UI kit", () => {
326
501
  expect(toggleMultiSelectValue(options, ["sma", "ema"], "sma")).toEqual(["ema"]);
327
502
  });
328
503
 
504
+ test("keeps ordered multi-select values in user order", () => {
505
+ const options = [
506
+ { value: "market", label: "MARKET" },
507
+ { value: "target", label: "TARGET" },
508
+ { value: "venue", label: "VENUE" },
509
+ { value: "odds", label: "TOP ODDS" },
510
+ ];
511
+
512
+ expect(normalizeOrderedMultiSelectValues(options, ["venue", "market"])).toEqual(["venue", "market"]);
513
+ expect(toggleOrderedMultiSelectValue(options, ["venue", "market"], "target")).toEqual(["venue", "market", "target"]);
514
+
515
+ const displayValues = getMultiSelectDisplayValues(options, ["venue", "market", "target"], true);
516
+ expect(orderMultiSelectOptionsForDisplay(options, displayValues).map((option) => option.value))
517
+ .toEqual(["venue", "market", "target", "odds"]);
518
+ expect(moveMultiSelectDisplayValue(displayValues, ["venue", "market", "target"], "market", "up"))
519
+ .toEqual(["market", "venue", "target", "odds"]);
520
+ });
521
+
329
522
  test("auto-scrolls a scrollable list to keep the selected row visible", async () => {
330
523
  testSetup = await testRender(
331
524
  <ScrollableListHarness />,
@@ -473,4 +666,24 @@ describe("shared UI kit", () => {
473
666
 
474
667
  expect(tableHorizontalScrollbarVisible).toBe(false);
475
668
  });
669
+
670
+ test("virtualizes data table rows by default", async () => {
671
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
672
+ testSetup = await testRender(
673
+ <AppContext value={{ state, dispatch: () => {} }}>
674
+ <PaneInstanceProvider paneId="portfolio-list:main">
675
+ <DataTableVirtualizationHarness />
676
+ </PaneInstanceProvider>
677
+ </AppContext>,
678
+ { width: 32, height: 6 },
679
+ );
680
+
681
+ await act(async () => {
682
+ await testSetup!.renderOnce();
683
+ });
684
+
685
+ const frame = testSetup.captureCharFrame();
686
+ expect(frame).toContain("Row 0");
687
+ expect(frame).not.toContain("Row 99");
688
+ });
476
689
  });
@@ -4,6 +4,10 @@ interface LastClickState {
4
4
  targetKey: string;
5
5
  }
6
6
 
7
+ interface ClickEventLike {
8
+ detail?: number;
9
+ }
10
+
7
11
  export function useDoubleClickActivation<T>({
8
12
  onSelect,
9
13
  onActivate,
@@ -13,12 +17,13 @@ export function useDoubleClickActivation<T>({
13
17
  }) {
14
18
  const lastClickRef = useRef<LastClickState | null>(null);
15
19
 
16
- return useCallback((targetKey: string, value: T) => {
20
+ return useCallback((targetKey: string, value: T, event?: ClickEventLike) => {
17
21
  const lastClick = lastClickRef.current;
18
22
 
19
23
  onSelect?.(value);
20
24
 
21
- if (lastClick && lastClick.targetKey === targetKey) {
25
+ if ((typeof event?.detail === "number" && event.detail >= 2)
26
+ || (lastClick && lastClick.targetKey === targetKey)) {
22
27
  lastClickRef.current = null;
23
28
  onActivate?.(value);
24
29
  return;
@@ -0,0 +1,79 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { useShortcut } from "../react/input";
3
+ import { useRendererHost } from "../ui";
4
+ import {
5
+ usePaneFooter,
6
+ type PaneFooterSegment,
7
+ type PaneHint,
8
+ } from "./layout/pane-footer";
9
+
10
+ interface UseExternalLinkFooterOptions {
11
+ registrationId: string;
12
+ focused: boolean;
13
+ url: string | null | undefined;
14
+ source?: string | null;
15
+ info?: PaneFooterSegment[];
16
+ hints?: PaneHint[];
17
+ label?: string;
18
+ }
19
+
20
+ const EMPTY_INFO: PaneFooterSegment[] = [];
21
+ const EMPTY_HINTS: PaneHint[] = [];
22
+
23
+ function normalizeUrl(url: string | null | undefined): string | null {
24
+ const trimmed = url?.trim();
25
+ return trimmed ? trimmed : null;
26
+ }
27
+
28
+ export function useExternalLinkFooter({
29
+ registrationId,
30
+ focused,
31
+ url: rawUrl,
32
+ source,
33
+ info = EMPTY_INFO,
34
+ hints = EMPTY_HINTS,
35
+ label = "link",
36
+ }: UseExternalLinkFooterOptions) {
37
+ const rendererHost = useRendererHost();
38
+ const url = normalizeUrl(rawUrl);
39
+ const sourceLabel = source?.trim() || null;
40
+
41
+ const openUrl = useCallback(() => {
42
+ if (!url) return;
43
+ void rendererHost.openExternal(url);
44
+ }, [rendererHost, url]);
45
+
46
+ useShortcut((event) => {
47
+ const key = (event.name ?? event.key ?? "").toLowerCase();
48
+ if (!focused || !url || key !== "o") return;
49
+ event.stopPropagation?.();
50
+ event.preventDefault?.();
51
+ openUrl();
52
+ });
53
+
54
+ const footer = useMemo(() => {
55
+ const contextLabel = label.trim() || "link";
56
+ const linkInfo: PaneFooterSegment[] = url && sourceLabel
57
+ ? [{
58
+ id: "external-link",
59
+ onPress: openUrl,
60
+ parts: [
61
+ { text: contextLabel === "link" ? "source" : contextLabel, tone: "label" as const },
62
+ { text: sourceLabel, tone: "value" as const },
63
+ ],
64
+ }]
65
+ : [];
66
+
67
+ return {
68
+ info: [...info, ...linkInfo],
69
+ hints: [
70
+ ...hints,
71
+ ...(url ? [{ id: "open", key: "o", label: "pen", onPress: openUrl }] : []),
72
+ ],
73
+ };
74
+ }, [hints, info, label, openUrl, sourceLabel, url]);
75
+
76
+ usePaneFooter(registrationId, () => footer, [footer]);
77
+
78
+ return openUrl;
79
+ }
@@ -0,0 +1,95 @@
1
+ import { join } from "path";
2
+ import { AppPersistence } from "../data/app-persistence";
3
+ import { TickerRepository } from "../data/ticker-repository";
4
+ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../market-data/coordinator";
5
+ import { NewsService } from "../news/aggregator";
6
+ import { setSharedNewsService } from "../news/hooks";
7
+ import { getLoadablePlugins } from "../plugins/catalog";
8
+ import type { LoadedExternalPlugin } from "../plugins/loader";
9
+ import { PluginRegistry } from "../plugins/registry";
10
+ import { AssetDataRouter } from "../sources/provider-router";
11
+ import { assetDataProvider, newsProvider } from "../capabilities";
12
+ import type { AppConfig } from "../types/config";
13
+ import type { DataProvider } from "../types/data-provider";
14
+ import { debugLog } from "../utils/debug-log";
15
+ import { measurePerf } from "../utils/perf-marks";
16
+
17
+ const servicesLog = debugLog.createLogger("services");
18
+
19
+ export interface AppServices {
20
+ persistence: AppPersistence;
21
+ tickerRepository: TickerRepository;
22
+ providerRouter: AssetDataRouter;
23
+ dataProvider: DataProvider;
24
+ marketData: MarketDataCoordinator;
25
+ pluginRegistry: PluginRegistry;
26
+ newsService: NewsService;
27
+ destroy(): void;
28
+ }
29
+
30
+ export function createAppServices({
31
+ config,
32
+ externalPlugins,
33
+ }: {
34
+ config: AppConfig;
35
+ externalPlugins: LoadedExternalPlugin[];
36
+ }): AppServices {
37
+ servicesLog.info("create services start", {
38
+ externalPluginCount: externalPlugins.length,
39
+ brokerInstanceCount: config.brokerInstances.length,
40
+ });
41
+ const dbPath = join(config.dataDir, ".gloomberb-cache.db");
42
+ const persistence = measurePerf("startup.services.persistence", () => new AppPersistence(dbPath));
43
+ const tickerRepository = measurePerf("startup.services.ticker-repository", () => new TickerRepository(persistence.tickers));
44
+ const providerRouter = measurePerf("startup.services.asset-data-router", () => new AssetDataRouter(null, [], persistence.resources));
45
+ const dataProvider: DataProvider = providerRouter;
46
+ const marketData = new MarketDataCoordinator(dataProvider);
47
+ const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository, persistence);
48
+ const newsService = new NewsService();
49
+ pluginRegistry.capabilities.register("core", assetDataProvider(providerRouter));
50
+ pluginRegistry.capabilities.register("core", newsProvider({
51
+ id: "core",
52
+ name: "News",
53
+ provider: {
54
+ fetchNews: async (query) => (await newsService.load(query)).articles,
55
+ getCachedNews: (query) => newsService.getQueryState(query).articles,
56
+ },
57
+ }));
58
+
59
+ providerRouter.attachRegistry(pluginRegistry);
60
+ pluginRegistry.getConfigFn = () => config;
61
+ pluginRegistry.getLayoutFn = () => config.layout;
62
+ pluginRegistry.registerNewsCapabilityFn = (capability) => newsService.register(capability);
63
+ pluginRegistry.watchNewsQueryFn = (query, listener) => newsService.watchQuery(query, listener);
64
+
65
+ setSharedNewsService(newsService);
66
+ setSharedMarketDataCoordinator(marketData);
67
+
68
+ const plugins = getLoadablePlugins(externalPlugins);
69
+ for (const plugin of plugins) {
70
+ measurePerf("startup.services.register-plugin", () => {
71
+ void pluginRegistry.register(plugin);
72
+ }, { pluginId: plugin.id });
73
+ }
74
+ measurePerf("startup.services.news-start", () => {
75
+ newsService.start();
76
+ });
77
+ servicesLog.info("create services complete", { pluginCount: plugins.length });
78
+
79
+ return {
80
+ persistence,
81
+ tickerRepository,
82
+ providerRouter,
83
+ dataProvider,
84
+ marketData,
85
+ pluginRegistry,
86
+ newsService,
87
+ destroy() {
88
+ setSharedMarketDataCoordinator(null);
89
+ setSharedNewsService(null);
90
+ newsService.stop();
91
+ pluginRegistry.destroy();
92
+ persistence.close();
93
+ },
94
+ };
95
+ }