gloomberb 0.5.1 → 0.6.1

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