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,22 +1,26 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { act, useEffect, useReducer } from "react";
3
- import { testRender } from "@opentui/react/test-utils";
2
+ import { act, useEffect, useReducer, type SetStateAction } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
4
  import {
5
5
  AppContext,
6
6
  PaneInstanceProvider,
7
7
  appReducer,
8
8
  createInitialState,
9
9
  } from "../state/app-context";
10
+ import { createConfigBackedTestPluginRuntime, createTestPluginRuntime } from "../test-support/plugin-runtime";
10
11
  import { createDefaultConfig } from "../types/config";
11
12
  import {
12
13
  PluginRenderProvider,
13
14
  deletePluginPaneStateValue,
14
15
  getPluginPaneStateValue,
15
16
  setPluginPaneStateValue,
16
- type PluginRuntimeAccess,
17
+ usePluginAppActions,
18
+ useDebouncedPluginPaneState,
17
19
  usePluginConfigState,
20
+ usePluginPaneActions,
18
21
  usePluginPaneState,
19
22
  usePluginState,
23
+ useSetPluginConfigStates,
20
24
  } from "./plugin-runtime";
21
25
 
22
26
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
@@ -51,77 +55,68 @@ describe("plugin runtime helpers", () => {
51
55
  });
52
56
 
53
57
  describe("plugin runtime hooks", () => {
58
+ test("debounces pane state commits", async () => {
59
+ const config = createDefaultConfig("/tmp/gloomberb-plugin-runtime-debounce");
60
+ const stateRef: { current: ReturnType<typeof createInitialState> | null } = { current: null };
61
+ let setSelection: ((value: SetStateAction<string>) => void) | null = null;
62
+
63
+ const runtime = createTestPluginRuntime();
64
+
65
+ function HookHarness() {
66
+ const [state, dispatch] = useReducer(appReducer, createInitialState(config));
67
+ stateRef.current = state;
68
+
69
+ return (
70
+ <AppContext value={{ state, dispatch }}>
71
+ <PaneInstanceProvider paneId="prediction-markets:main">
72
+ <PluginRenderProvider pluginId="prediction-markets" runtime={runtime}>
73
+ <HookProbe />
74
+ </PluginRenderProvider>
75
+ </PaneInstanceProvider>
76
+ </AppContext>
77
+ );
78
+ }
79
+
80
+ function HookProbe() {
81
+ const [selection, setDebouncedSelection] = useDebouncedPluginPaneState("selectedRowKey", "row-a", 20);
82
+ setSelection = setDebouncedSelection;
83
+ return <text>{selection}</text>;
84
+ }
85
+
86
+ testSetup = await testRender(<HookHarness />, { width: 40, height: 5 });
87
+
88
+ await act(async () => {
89
+ await testSetup!.renderOnce();
90
+ });
91
+
92
+ await act(async () => {
93
+ setSelection?.("row-b");
94
+ await testSetup!.renderOnce();
95
+ await testSetup!.renderOnce();
96
+ });
97
+
98
+ expect(
99
+ stateRef.current?.paneState["prediction-markets:main"]?.pluginState?.["prediction-markets"]?.selectedRowKey,
100
+ ).toBeUndefined();
101
+
102
+ await act(async () => {
103
+ await new Promise((resolve) => setTimeout(resolve, 30));
104
+ await testSetup!.renderOnce();
105
+ });
106
+
107
+ expect(
108
+ stateRef.current?.paneState["prediction-markets:main"]?.pluginState?.["prediction-markets"]?.selectedRowKey,
109
+ ).toBe("row-b");
110
+ });
111
+
54
112
  test("updates pane, global resume, and config state through the plugin hooks", async () => {
55
113
  const config = createDefaultConfig("/tmp/gloomberb-plugin-runtime");
56
114
  const stateRef: { current: ReturnType<typeof createInitialState> | null } = { current: null };
57
115
  const dispatchRef: { current: React.Dispatch<any> | null } = { current: null };
58
- const resumeState = new Map<string, unknown>();
59
- const listeners = new Map<string, Set<() => void>>();
60
-
61
- const runtime: PluginRuntimeAccess = {
62
- pinTicker() {},
63
- navigateTicker() {},
64
- subscribeResumeState(pluginId, key, listener) {
65
- const listenerKey = `${pluginId}:${key}`;
66
- if (!listeners.has(listenerKey)) listeners.set(listenerKey, new Set());
67
- listeners.get(listenerKey)!.add(listener);
68
- return () => {
69
- const current = listeners.get(listenerKey);
70
- if (!current) return;
71
- current.delete(listener);
72
- };
73
- },
74
- getResumeState(pluginId, key) {
75
- return (resumeState.get(`${pluginId}:${key}`) as any) ?? null;
76
- },
77
- setResumeState(pluginId, key, value) {
78
- const listenerKey = `${pluginId}:${key}`;
79
- resumeState.set(listenerKey, value);
80
- for (const listener of listeners.get(listenerKey) ?? []) listener();
81
- },
82
- deleteResumeState(pluginId, key) {
83
- const listenerKey = `${pluginId}:${key}`;
84
- resumeState.delete(listenerKey);
85
- for (const listener of listeners.get(listenerKey) ?? []) listener();
86
- },
87
- getConfigState(pluginId, key) {
88
- return (stateRef.current?.config.pluginConfig[pluginId]?.[key] as any) ?? null;
89
- },
90
- async setConfigState(pluginId, key, value) {
91
- const currentState = stateRef.current!;
92
- dispatchRef.current?.({
93
- type: "SET_CONFIG",
94
- config: {
95
- ...currentState.config,
96
- pluginConfig: {
97
- ...currentState.config.pluginConfig,
98
- [pluginId]: {
99
- ...(currentState.config.pluginConfig[pluginId] ?? {}),
100
- [key]: value,
101
- },
102
- },
103
- },
104
- });
105
- },
106
- async deleteConfigState(pluginId, key) {
107
- const currentState = stateRef.current!;
108
- const currentPluginConfig = { ...(currentState.config.pluginConfig[pluginId] ?? {}) };
109
- delete currentPluginConfig[key];
110
- const nextPluginConfig = { ...currentState.config.pluginConfig };
111
- if (Object.keys(currentPluginConfig).length === 0) delete nextPluginConfig[pluginId];
112
- else nextPluginConfig[pluginId] = currentPluginConfig;
113
- dispatchRef.current?.({
114
- type: "SET_CONFIG",
115
- config: {
116
- ...currentState.config,
117
- pluginConfig: nextPluginConfig,
118
- },
119
- });
120
- },
121
- getConfigStateKeys(pluginId) {
122
- return Object.keys(stateRef.current?.config.pluginConfig[pluginId] ?? {}).sort();
123
- },
124
- };
116
+ const runtime = createConfigBackedTestPluginRuntime({
117
+ getConfig: () => stateRef.current?.config,
118
+ setConfig: (config) => dispatchRef.current?.({ type: "SET_CONFIG", config }),
119
+ });
125
120
 
126
121
  function HookHarness() {
127
122
  const [state, dispatch] = useReducer(appReducer, createInitialState(config));
@@ -144,16 +139,24 @@ describe("plugin runtime hooks", () => {
144
139
  const [venueScope, setVenueScope] = usePluginPaneState<string>("venueScope", "all");
145
140
  const [provider, setProvider] = usePluginState<string>("provider", "claude");
146
141
  const [mode, setMode] = usePluginConfigState<string>("displayMode", "compact");
142
+ const [layoutMode] = usePluginConfigState<string>("layoutMode", "default");
143
+ const setConfigStates = useSetPluginConfigStates();
147
144
 
148
145
  useEffect(() => {
149
146
  if (paneSelection === 0) setPaneSelection(3);
150
147
  if (venueScope === "all") setVenueScope("kalshi");
151
148
  if (provider === "claude") setProvider("codex");
152
- if (mode === "compact") setMode("expanded");
149
+ if (mode === "compact") {
150
+ setMode("expanded");
151
+ } else if (layoutMode === "default") {
152
+ setConfigStates({ layoutMode: "wide", density: "dense" });
153
+ }
153
154
  }, [
155
+ layoutMode,
154
156
  mode,
155
157
  paneSelection,
156
158
  provider,
159
+ setConfigStates,
157
160
  setMode,
158
161
  setPaneSelection,
159
162
  setProvider,
@@ -161,7 +164,7 @@ describe("plugin runtime hooks", () => {
161
164
  venueScope,
162
165
  ]);
163
166
 
164
- return <text>{`${paneSelection}|${venueScope}|${provider}|${mode}`}</text>;
167
+ return <text>{`${paneSelection}|${venueScope}|${provider}|${mode}|${layoutMode}`}</text>;
165
168
  }
166
169
 
167
170
  testSetup = await testRender(<HookHarness />, { width: 40, height: 5 });
@@ -169,10 +172,11 @@ describe("plugin runtime hooks", () => {
169
172
  await act(async () => {
170
173
  await testSetup!.renderOnce();
171
174
  await testSetup!.renderOnce();
175
+ await testSetup!.renderOnce();
172
176
  });
173
177
 
174
178
  const frame = testSetup.captureCharFrame();
175
- expect(frame).toContain("3|kalshi|codex|expanded");
179
+ expect(frame).toContain("3|kalshi|codex|expanded|wide");
176
180
  expect(stateRef.current?.paneState["portfolio-list:main"]?.pluginState).toEqual({
177
181
  news: {
178
182
  selectedIdx: 3,
@@ -180,8 +184,106 @@ describe("plugin runtime hooks", () => {
180
184
  },
181
185
  });
182
186
  expect(stateRef.current?.config.pluginConfig.news).toEqual({
187
+ density: "dense",
183
188
  displayMode: "expanded",
189
+ layoutMode: "wide",
190
+ });
191
+ expect(runtime.getResumeState("news", "provider")).toBe("codex");
192
+ });
193
+
194
+ test("exposes renderer app actions through the plugin hook", async () => {
195
+ const calls: string[] = [];
196
+ let actions: ReturnType<typeof usePluginAppActions> | null = null;
197
+
198
+ const runtime = createTestPluginRuntime({
199
+ openCommandBar(query?: string) {
200
+ calls.push(`command:${query ?? ""}`);
201
+ },
202
+ showWidget(widgetId: string) {
203
+ calls.push(`show:${widgetId}`);
204
+ },
205
+ hideWidget(widgetId: string) {
206
+ calls.push(`hide:${widgetId}`);
207
+ },
208
+ openPluginCommandWorkflow(commandId: string) {
209
+ calls.push(`workflow:${commandId}`);
210
+ },
211
+ notify(notification) {
212
+ calls.push(`notify:${notification.body}`);
213
+ },
214
+ });
215
+
216
+ function HookProbe() {
217
+ actions = usePluginAppActions();
218
+ return <text>actions</text>;
219
+ }
220
+
221
+ testSetup = await testRender(
222
+ <PluginRenderProvider pluginId="help" runtime={runtime}>
223
+ <HookProbe />
224
+ </PluginRenderProvider>,
225
+ { width: 40, height: 5 },
226
+ );
227
+
228
+ await act(async () => {
229
+ await testSetup!.renderOnce();
230
+ });
231
+
232
+ actions?.openCommandBar("PL ");
233
+ actions?.showWidget("debug");
234
+ actions?.hideWidget("chat");
235
+ actions?.openPluginCommandWorkflow("set-alert");
236
+ actions?.notify({ body: "Saved", type: "success" });
237
+
238
+ expect(calls).toEqual([
239
+ "command:PL ",
240
+ "show:debug",
241
+ "hide:chat",
242
+ "workflow:set-alert",
243
+ "notify:Saved",
244
+ ]);
245
+ });
246
+
247
+ test("exposes renderer pane actions through the plugin hook", async () => {
248
+ const calls: string[] = [];
249
+ let actions: ReturnType<typeof usePluginPaneActions> | null = null;
250
+
251
+ const runtime = createTestPluginRuntime({
252
+ selectTicker(symbol: string, paneId?: string) {
253
+ calls.push(`select:${symbol}:${paneId ?? ""}`);
254
+ },
255
+ switchTab(tabId: string, paneId?: string) {
256
+ calls.push(`tab:${tabId}:${paneId ?? ""}`);
257
+ },
258
+ switchPanel(panel: "left" | "right") {
259
+ calls.push(`panel:${panel}`);
260
+ },
184
261
  });
185
- expect(resumeState.get("news:provider")).toBe("codex");
262
+
263
+ function HookProbe() {
264
+ actions = usePluginPaneActions();
265
+ return <text>pane-actions</text>;
266
+ }
267
+
268
+ testSetup = await testRender(
269
+ <PluginRenderProvider pluginId="ibkr" runtime={runtime}>
270
+ <HookProbe />
271
+ </PluginRenderProvider>,
272
+ { width: 40, height: 5 },
273
+ );
274
+
275
+ await act(async () => {
276
+ await testSetup!.renderOnce();
277
+ });
278
+
279
+ actions?.selectTicker("AAPL", "ibkr:main");
280
+ actions?.switchTab("ibkr-trade", "ibkr:main");
281
+ actions?.switchPanel("right");
282
+
283
+ expect(calls).toEqual([
284
+ "select:AAPL:ibkr:main",
285
+ "tab:ibkr-trade:ibkr:main",
286
+ "panel:right",
287
+ ]);
186
288
  });
187
289
  });
@@ -2,22 +2,50 @@ import {
2
2
  createContext,
3
3
  useCallback,
4
4
  useContext,
5
+ useEffect,
5
6
  useRef,
7
+ useState,
6
8
  useSyncExternalStore,
7
9
  type ReactNode,
8
10
  type SetStateAction,
9
11
  } from "react";
10
- import { useAppState, usePaneInstanceId, type PaneRuntimeState } from "../state/app-context";
12
+ import {
13
+ useAppDispatch,
14
+ useAppSelector,
15
+ useAppStateRef,
16
+ usePaneInstanceId,
17
+ type PaneRuntimeState,
18
+ } from "../state/app-context";
19
+ import type { BrokerAdapter } from "../types/broker";
20
+ import type { PluginCapability } from "../capabilities";
21
+ import type { DataProvider } from "../types/data-provider";
22
+ import type { AppNotificationRequest, BrokerInstanceUpdateOptions } from "../types/plugin";
11
23
 
12
24
  export interface PluginRuntimeAccess {
25
+ getMarketData(): DataProvider | null;
26
+ getCapability(capabilityId: string): PluginCapability | null;
27
+ getBrokerAdapter(brokerType: string): BrokerAdapter | null;
28
+ connectBrokerInstance(instanceId: string): Promise<void>;
29
+ updateBrokerInstance(instanceId: string, values: Record<string, unknown>, options?: BrokerInstanceUpdateOptions): Promise<void>;
30
+ syncBrokerInstance(instanceId: string): Promise<void>;
31
+ removeBrokerInstance(instanceId: string): Promise<void>;
13
32
  pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
14
33
  navigateTicker(symbol: string): void;
34
+ selectTicker(symbol: string, paneId?: string): void;
35
+ switchTab(tabId: string, paneId?: string): void;
36
+ switchPanel(panel: "left" | "right"): void;
37
+ openCommandBar(query?: string): void;
38
+ showWidget(widgetId: string): void;
39
+ hideWidget(widgetId: string): void;
40
+ openPluginCommandWorkflow(commandId: string): void;
41
+ notify(notification: AppNotificationRequest): void;
15
42
  subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
16
43
  getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null;
17
44
  setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void;
18
45
  deleteResumeState(pluginId: string, key: string): void;
19
46
  getConfigState<T = unknown>(pluginId: string, key: string): T | null;
20
47
  setConfigState(pluginId: string, key: string, value: unknown): Promise<void>;
48
+ setConfigStates(pluginId: string, values: Record<string, unknown>): Promise<void>;
21
49
  deleteConfigState(pluginId: string, key: string): Promise<void>;
22
50
  getConfigStateKeys(pluginId: string): string[];
23
51
  }
@@ -28,6 +56,7 @@ interface PluginRenderContextValue {
28
56
  }
29
57
 
30
58
  const PluginRenderContext = createContext<PluginRenderContextValue | null>(null);
59
+ const DEFAULT_PLUGIN_PANE_STATE_COMMIT_DELAY_MS = 300;
31
60
 
32
61
  export function PluginRenderProvider({
33
62
  pluginId,
@@ -61,6 +90,81 @@ export function usePluginTickerActions() {
61
90
  };
62
91
  }
63
92
 
93
+ export function usePluginPaneActions() {
94
+ const { runtime } = usePluginRenderContext();
95
+ const selectTicker = useCallback((symbol: string, paneId?: string) => {
96
+ runtime.selectTicker(symbol, paneId);
97
+ }, [runtime]);
98
+ const switchTab = useCallback((tabId: string, paneId?: string) => {
99
+ runtime.switchTab(tabId, paneId);
100
+ }, [runtime]);
101
+ const switchPanel = useCallback((panel: "left" | "right") => {
102
+ runtime.switchPanel(panel);
103
+ }, [runtime]);
104
+
105
+ return {
106
+ selectTicker,
107
+ switchTab,
108
+ switchPanel,
109
+ };
110
+ }
111
+
112
+ export function usePluginAppActions() {
113
+ const { runtime } = usePluginRenderContext();
114
+ const openCommandBar = useCallback((query?: string) => {
115
+ runtime.openCommandBar(query);
116
+ }, [runtime]);
117
+ const showWidget = useCallback((widgetId: string) => {
118
+ runtime.showWidget(widgetId);
119
+ }, [runtime]);
120
+ const hideWidget = useCallback((widgetId: string) => {
121
+ runtime.hideWidget(widgetId);
122
+ }, [runtime]);
123
+ const openPluginCommandWorkflow = useCallback((commandId: string) => {
124
+ runtime.openPluginCommandWorkflow(commandId);
125
+ }, [runtime]);
126
+ const notify = useCallback((notification: AppNotificationRequest) => {
127
+ runtime.notify(notification);
128
+ }, [runtime]);
129
+
130
+ return {
131
+ openCommandBar,
132
+ showWidget,
133
+ hideWidget,
134
+ openPluginCommandWorkflow,
135
+ notify,
136
+ };
137
+ }
138
+
139
+ export function useMarketData(): DataProvider | null {
140
+ const { runtime } = usePluginRenderContext();
141
+ return runtime.getMarketData();
142
+ }
143
+
144
+ export function useAssetData(): DataProvider | null {
145
+ return useMarketData();
146
+ }
147
+
148
+ export function useCapability(capabilityId: string): PluginCapability | null {
149
+ const { runtime } = usePluginRenderContext();
150
+ return runtime.getCapability(capabilityId);
151
+ }
152
+
153
+ export function useNewsData(): PluginCapability | null {
154
+ return useCapability("news.core");
155
+ }
156
+
157
+ export function usePluginBrokerActions() {
158
+ const { runtime } = usePluginRenderContext();
159
+ return {
160
+ getBrokerAdapter: runtime.getBrokerAdapter,
161
+ connectBrokerInstance: runtime.connectBrokerInstance,
162
+ updateBrokerInstance: runtime.updateBrokerInstance,
163
+ syncBrokerInstance: runtime.syncBrokerInstance,
164
+ removeBrokerInstance: runtime.removeBrokerInstance,
165
+ };
166
+ }
167
+
64
168
  export function getPluginPaneStateValue<T>(
65
169
  paneState: PaneRuntimeState | undefined,
66
170
  pluginId: string,
@@ -111,13 +215,13 @@ export function deletePluginPaneStateValue(
111
215
  export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string): [T, (value: SetStateAction<T>) => void] {
112
216
  const { pluginId } = usePluginRenderContext();
113
217
  const scopedPaneId = paneId ?? usePaneInstanceId();
114
- const { state, dispatch } = useAppState();
115
- const stateRef = useRef(state);
218
+ const dispatch = useAppDispatch();
219
+ const stateRef = useAppStateRef();
116
220
  const fallbackRef = useRef(fallback);
117
- stateRef.current = state;
118
221
  fallbackRef.current = fallback;
119
- const paneState = state.paneState[scopedPaneId];
120
- const value = getPluginPaneStateValue(paneState, pluginId, key, fallback);
222
+ const value = useAppSelector((state) => (
223
+ getPluginPaneStateValue(state.paneState[scopedPaneId], pluginId, key, fallback)
224
+ ));
121
225
 
122
226
  const setValue = useCallback((nextValue: SetStateAction<T>) => {
123
227
  const currentPaneState = stateRef.current.paneState[scopedPaneId];
@@ -147,6 +251,109 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
147
251
  return [value, setValue];
148
252
  }
149
253
 
254
+ export function useDebouncedPluginPaneState<T>(
255
+ key: string,
256
+ fallback: T,
257
+ delayMs = DEFAULT_PLUGIN_PANE_STATE_COMMIT_DELAY_MS,
258
+ paneId?: string,
259
+ ): [
260
+ T,
261
+ (value: SetStateAction<T>, options?: { immediate?: boolean }) => void,
262
+ () => void,
263
+ ] {
264
+ const [committedValue, setCommittedValue] = usePluginPaneState<T>(key, fallback, paneId);
265
+ const [localValue, setLocalValueState] = useState(committedValue);
266
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
267
+ const hasPendingCommitRef = useRef(false);
268
+ const pendingValueRef = useRef(committedValue);
269
+ const appliedValueRef = useRef(committedValue);
270
+ const localValueRef = useRef(committedValue);
271
+
272
+ const clearPendingCommit = useCallback(() => {
273
+ if (timerRef.current) {
274
+ clearTimeout(timerRef.current);
275
+ timerRef.current = null;
276
+ }
277
+ hasPendingCommitRef.current = false;
278
+ }, []);
279
+
280
+ const commitValue = useCallback((nextValue?: T) => {
281
+ const targetValue = nextValue ?? pendingValueRef.current;
282
+ clearPendingCommit();
283
+ pendingValueRef.current = targetValue;
284
+
285
+ if (Object.is(appliedValueRef.current, targetValue)) {
286
+ return;
287
+ }
288
+
289
+ appliedValueRef.current = targetValue;
290
+ setCommittedValue(targetValue);
291
+ }, [clearPendingCommit, setCommittedValue]);
292
+
293
+ const setLocalValue = useCallback((
294
+ nextValue: SetStateAction<T>,
295
+ options?: { immediate?: boolean },
296
+ ) => {
297
+ const currentValue = localValueRef.current;
298
+ const resolved = typeof nextValue === "function"
299
+ ? (nextValue as (previousValue: T) => T)(currentValue)
300
+ : nextValue;
301
+
302
+ if (Object.is(resolved, currentValue)) {
303
+ if (options?.immediate) {
304
+ commitValue(resolved);
305
+ }
306
+ return;
307
+ }
308
+
309
+ localValueRef.current = resolved;
310
+ pendingValueRef.current = resolved;
311
+ setLocalValueState((current) => (Object.is(current, resolved) ? current : resolved));
312
+
313
+ if (options?.immediate) {
314
+ commitValue(resolved);
315
+ return;
316
+ }
317
+
318
+ clearPendingCommit();
319
+ if (Object.is(appliedValueRef.current, resolved)) {
320
+ return;
321
+ }
322
+
323
+ hasPendingCommitRef.current = true;
324
+ timerRef.current = setTimeout(() => {
325
+ commitValue();
326
+ }, delayMs);
327
+ }, [clearPendingCommit, commitValue, delayMs]);
328
+
329
+ useEffect(() => {
330
+ if (hasPendingCommitRef.current && Object.is(committedValue, pendingValueRef.current)) {
331
+ clearPendingCommit();
332
+ }
333
+
334
+ if (!hasPendingCommitRef.current) {
335
+ appliedValueRef.current = committedValue;
336
+ pendingValueRef.current = committedValue;
337
+ localValueRef.current = committedValue;
338
+ setLocalValueState((current) => (Object.is(current, committedValue) ? current : committedValue));
339
+ }
340
+ }, [clearPendingCommit, committedValue]);
341
+
342
+ useEffect(() => () => {
343
+ const pending = hasPendingCommitRef.current;
344
+ const pendingValue = pendingValueRef.current;
345
+ if (timerRef.current) {
346
+ clearTimeout(timerRef.current);
347
+ timerRef.current = null;
348
+ }
349
+ if (pending && !Object.is(appliedValueRef.current, pendingValue)) {
350
+ setCommittedValue(pendingValue);
351
+ }
352
+ }, [setCommittedValue]);
353
+
354
+ return [localValue, setLocalValue, commitValue];
355
+ }
356
+
150
357
  export function usePluginState<T>(key: string, fallback: T, options?: { schemaVersion?: number }): [T, (value: SetStateAction<T>) => void] {
151
358
  const { pluginId, runtime } = usePluginRenderContext();
152
359
  const schemaVersion = options?.schemaVersion;
@@ -181,12 +388,12 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
181
388
 
182
389
  export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: SetStateAction<T>) => void] {
183
390
  const { pluginId, runtime } = usePluginRenderContext();
184
- const { state } = useAppState();
185
- const stateRef = useRef(state);
391
+ const stateRef = useAppStateRef();
186
392
  const fallbackRef = useRef(fallback);
187
- stateRef.current = state;
188
393
  fallbackRef.current = fallback;
189
- const value = (state.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? fallback;
394
+ const value = useAppSelector((state) => (
395
+ (state.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? fallback
396
+ ));
190
397
 
191
398
  const setValue = useCallback((nextValue: SetStateAction<T>) => {
192
399
  const currentValue =
@@ -203,3 +410,14 @@ export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: S
203
410
 
204
411
  return [value, setValue];
205
412
  }
413
+
414
+ export function useSetPluginConfigStates(): (values: Record<string, unknown>) => void {
415
+ const { pluginId, runtime } = usePluginRenderContext();
416
+
417
+ return useCallback((values: Record<string, unknown>) => {
418
+ if (Object.keys(values).length === 0) {
419
+ return;
420
+ }
421
+ void runtime.setConfigStates(pluginId, values);
422
+ }, [pluginId, runtime]);
423
+ }
@@ -112,3 +112,52 @@ export function updatePredictionErrorState(
112
112
  [key]: value,
113
113
  };
114
114
  }
115
+
116
+ function sameNullableNumber(
117
+ left: number | null | undefined,
118
+ right: number | null | undefined,
119
+ ): boolean {
120
+ return (left ?? null) === (right ?? null);
121
+ }
122
+
123
+ function sameSummaryForCatalog(
124
+ left: PredictionMarketSummary,
125
+ right: PredictionMarketSummary,
126
+ ): boolean {
127
+ return (
128
+ left.key === right.key &&
129
+ left.title === right.title &&
130
+ left.marketLabel === right.marketLabel &&
131
+ left.eventLabel === right.eventLabel &&
132
+ left.category === right.category &&
133
+ left.status === right.status &&
134
+ left.endsAt === right.endsAt &&
135
+ left.updatedAt === right.updatedAt &&
136
+ sameNullableNumber(left.yesPrice, right.yesPrice) &&
137
+ sameNullableNumber(left.noPrice, right.noPrice) &&
138
+ sameNullableNumber(left.yesBid, right.yesBid) &&
139
+ sameNullableNumber(left.yesAsk, right.yesAsk) &&
140
+ sameNullableNumber(left.noBid, right.noBid) &&
141
+ sameNullableNumber(left.noAsk, right.noAsk) &&
142
+ sameNullableNumber(left.spread, right.spread) &&
143
+ sameNullableNumber(left.lastTradePrice, right.lastTradePrice) &&
144
+ sameNullableNumber(left.volume24h, right.volume24h) &&
145
+ sameNullableNumber(left.totalVolume, right.totalVolume) &&
146
+ sameNullableNumber(left.openInterest, right.openInterest) &&
147
+ sameNullableNumber(left.liquidity, right.liquidity)
148
+ );
149
+ }
150
+
151
+ export function samePredictionCatalogSummaries(
152
+ left: readonly PredictionMarketSummary[] | undefined,
153
+ right: readonly PredictionMarketSummary[],
154
+ ): boolean {
155
+ if (!left || left.length !== right.length) return false;
156
+ for (let index = 0; index < left.length; index += 1) {
157
+ const leftSummary = left[index];
158
+ const rightSummary = right[index];
159
+ if (!leftSummary || !rightSummary) return false;
160
+ if (!sameSummaryForCatalog(leftSummary, rightSummary)) return false;
161
+ }
162
+ return true;
163
+ }
@@ -183,16 +183,6 @@ export function matchesPredictionCategory(
183
183
  return keywords.some((keyword) => haystack.includes(keyword));
184
184
  }
185
185
 
186
- export function getAvailablePredictionCategories(
187
- markets: PredictionMarketSummary[],
188
- ): PredictionCategoryOption[] {
189
- return PREDICTION_CATEGORY_OPTIONS.filter(
190
- (category) =>
191
- category.id === "all" ||
192
- markets.some((market) => matchesPredictionCategory(market, category.id)),
193
- );
194
- }
195
-
196
186
  export function getPolymarketCategoryTagSlugs(
197
187
  categoryId: PredictionCategoryId,
198
188
  ): string[] {
@@ -1,7 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import type { ScrollBoxRenderable } from "@opentui/core";
3
3
  import { createTestRenderer } from "@opentui/core/testing";
4
- import { createRoot } from "@opentui/react";
4
+ import { createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
5
5
  import { act, useEffect, useReducer, useRef } from "react";
6
6
  import {
7
7
  AppContext,