gloomberb 0.5.1 → 0.6.0

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