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,30 +1,107 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { PageStackView, TabBar } from "../../components";
1
+ import { Box, Input, Text } from "../../ui";
2
+ import { useCallback, useMemo, useRef } from "react";
3
+ import { DataTableStackView, Tabs, usePaneFooter } from "../../components";
4
+ import { createRowValueCache } from "../../components/ui/row-value-cache";
3
5
  import type { PaneProps } from "../../types/plugin";
4
6
  import { colors } from "../../theme/colors";
5
7
  import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
6
8
  import { usePredictionMarketsController } from "./controller";
7
9
  import { PredictionMarketDetailPane } from "./detail/pane";
10
+ import { getPredictionColumnValue } from "./metrics";
8
11
  import { BROWSE_TABS, VENUE_TABS } from "./navigation";
9
- import { PredictionMarketsTable } from "./table";
10
- import type { PredictionBrowseTab, PredictionCategoryId } from "./types";
12
+ import type {
13
+ PredictionBrowseTab,
14
+ PredictionCategoryId,
15
+ PredictionColumnDef,
16
+ PredictionListRow,
17
+ } from "./types";
18
+
19
+ const PREDICTION_CELL_CACHE_SIZE = 12_000;
20
+ const RELATIVE_TIME_CELL_BUCKET_MS = 60_000;
21
+
22
+ const predictionRowVersions = new WeakMap<object, number>();
23
+ let nextPredictionRowVersion = 1;
24
+
25
+ function predictionRowVersion(row: PredictionListRow): number {
26
+ const existing = predictionRowVersions.get(row);
27
+ if (existing != null) return existing;
28
+ const next = nextPredictionRowVersion;
29
+ nextPredictionRowVersion += 1;
30
+ predictionRowVersions.set(row, next);
31
+ return next;
32
+ }
33
+
34
+ function predictionCellVersion(
35
+ row: PredictionListRow,
36
+ column: PredictionColumnDef,
37
+ watchlisted: boolean,
38
+ relativeTimeBucket: number,
39
+ ): string {
40
+ return [
41
+ predictionRowVersion(row),
42
+ column.id,
43
+ watchlisted ? 1 : 0,
44
+ column.id === "ends" || column.id === "updated" ? relativeTimeBucket : 0,
45
+ ].join("|");
46
+ }
11
47
 
12
48
  export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
13
49
  const controller = usePredictionMarketsController({ focused });
50
+ const cellCacheRef = useRef(
51
+ createRowValueCache<string, ReturnType<typeof getPredictionColumnValue>>(
52
+ PREDICTION_CELL_CACHE_SIZE,
53
+ ),
54
+ );
55
+ const relativeTimeBucket = Math.floor(Date.now() / RELATIVE_TIME_CELL_BUCKET_MS);
56
+ const watchlistedRowKeys = useMemo(() => {
57
+ const keys = new Set<string>();
58
+ for (const row of controller.visibleRows) {
59
+ if (row.watchMarketKeys.some((marketKey) => controller.watchlistSet.has(marketKey))) {
60
+ keys.add(row.key);
61
+ }
62
+ }
63
+ return keys;
64
+ }, [controller.visibleRows, controller.watchlistSet]);
14
65
  const catalogStatusColor =
15
66
  controller.catalogStatus?.tone === "danger"
16
67
  ? colors.negative
17
68
  : colors.borderFocused;
69
+ usePaneFooter("prediction-markets", () => ({
70
+ info: [
71
+ ...(controller.searchQuery.trim() ? [{ id: "search", parts: [{ text: `search: ${controller.searchQuery.trim()}`, tone: "value" as const }] }] : []),
72
+ ...(controller.searchLoading ? [{ id: "search-loading", parts: [{ text: "searching", tone: "muted" as const }] }] : []),
73
+ ...(controller.catalogStatus ? [{
74
+ id: "catalog",
75
+ parts: [{ text: controller.catalogStatus.message, tone: controller.catalogStatus.tone === "danger" ? "warning" as const : "muted" as const, color: catalogStatusColor }],
76
+ }] : []),
77
+ ],
78
+ hints: [
79
+ { id: "search", key: "/", label: "search", onPress: controller.actions.focusSearch },
80
+ { id: "watch", key: "w", label: "atch", onPress: controller.selectedRow ? () => controller.actions.toggleWatchlist(controller.selectedRow!) : undefined, disabled: !controller.selectedRow },
81
+ {
82
+ id: "browse",
83
+ key: "1-4",
84
+ label: "browse",
85
+ onPress: () => {
86
+ const index = BROWSE_TABS.findIndex((tab) => tab.value === controller.browseTab);
87
+ controller.actions.selectBrowseTab(BROWSE_TABS[(index + 1) % BROWSE_TABS.length]!.value as PredictionBrowseTab);
88
+ },
89
+ },
90
+ ],
91
+ }), [
92
+ catalogStatusColor,
93
+ controller.browseTab,
94
+ controller.catalogStatus?.message,
95
+ controller.catalogStatus?.tone,
96
+ controller.searchLoading,
97
+ controller.searchQuery,
98
+ controller.selectedRow,
99
+ ]);
18
100
 
19
- const browseContent = (
20
- <box
21
- flexDirection="column"
22
- width={width}
23
- height={height}
24
- backgroundColor={colors.panel}
25
- >
101
+ const browseControls = (
102
+ <>
26
103
  {!controller.paneSettings.hideTabs ? (
27
- <TabBar
104
+ <Tabs
28
105
  tabs={VENUE_TABS.map((tab) => ({
29
106
  label: tab.label,
30
107
  value: tab.value,
@@ -35,16 +112,16 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
35
112
  />
36
113
  ) : null}
37
114
 
38
- <box flexDirection="row" height={1} paddingX={1} gap={2}>
39
- <box
115
+ <Box flexDirection="row" height={1} paddingX={1} gap={2}>
116
+ <Box
40
117
  flexDirection="row"
41
118
  onMouseDown={controller.actions.focusSearch}
42
119
  width={Math.max(18, Math.floor(width * 0.32))}
43
120
  >
44
- <text fg={colors.textDim}>{controller.searchFocused ? "?" : "/"}</text>
45
- <box width={1} />
121
+ <Text fg={colors.textDim}>{controller.searchFocused ? "?" : "/"}</Text>
122
+ <Box width={1} />
46
123
  {controller.searchFocused ? (
47
- <input
124
+ <Input
48
125
  ref={controller.searchInputRef}
49
126
  value={controller.searchQuery}
50
127
  focused={focused}
@@ -58,8 +135,8 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
58
135
  onSubmit={controller.actions.blurSearch}
59
136
  />
60
137
  ) : (
61
- <box flexGrow={1}>
62
- <text
138
+ <Box flexGrow={1}>
139
+ <Text
63
140
  fg={
64
141
  controller.searchQuery.trim().length > 0
65
142
  ? colors.text
@@ -69,13 +146,13 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
69
146
  {controller.searchQuery.trim().length > 0
70
147
  ? controller.searchQuery
71
148
  : "search markets"}
72
- </text>
73
- </box>
149
+ </Text>
150
+ </Box>
74
151
  )}
75
- </box>
152
+ </Box>
76
153
 
77
- <box flexGrow={1}>
78
- <TabBar
154
+ <Box flexGrow={1}>
155
+ <Tabs
79
156
  tabs={BROWSE_TABS.map((tab) => ({
80
157
  label: tab.label,
81
158
  value: tab.value,
@@ -86,68 +163,63 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
86
163
  }
87
164
  compact
88
165
  />
89
- </box>
90
- </box>
166
+ </Box>
167
+ </Box>
91
168
 
92
169
  {PREDICTION_CATEGORY_OPTIONS.length > 1 ? (
93
- <scrollbox height={1} scrollX focusable={false}>
94
- <box flexDirection="row" paddingX={1} gap={2}>
95
- {PREDICTION_CATEGORY_OPTIONS.map((category) => {
96
- const active = category.id === controller.categoryId;
97
- return (
98
- <box
99
- key={category.id}
100
- onMouseDown={(event) => {
101
- event.preventDefault();
102
- controller.actions.selectCategory(
103
- category.id as PredictionCategoryId,
104
- );
105
- }}
106
- >
107
- <text
108
- fg={active ? colors.textBright : colors.textDim}
109
- attributes={active ? TextAttributes.BOLD : 0}
110
- >
111
- {category.label}
112
- </text>
113
- </box>
114
- );
115
- })}
116
- </box>
117
- </scrollbox>
118
- ) : null}
119
-
120
- {controller.catalogStatus ? (
121
- <box height={1} paddingX={1} width={width} backgroundColor={colors.panel}>
122
- <text fg={catalogStatusColor}>{controller.catalogStatus.message}</text>
123
- </box>
170
+ <Box height={1} paddingX={1}>
171
+ <Tabs
172
+ tabs={PREDICTION_CATEGORY_OPTIONS.map((category) => ({
173
+ label: category.label,
174
+ value: category.id,
175
+ }))}
176
+ activeValue={controller.categoryId}
177
+ onSelect={(value) =>
178
+ controller.actions.selectCategory(value as PredictionCategoryId)
179
+ }
180
+ compact
181
+ variant="bare"
182
+ />
183
+ </Box>
124
184
  ) : null}
125
185
 
126
- <box flexDirection="column" flexGrow={1}>
127
- <PredictionMarketsTable
128
- columns={controller.visibleColumns}
129
- rows={controller.visibleRows}
130
- selectedRowKey={controller.selectedRow?.key ?? null}
131
- hoveredIdx={controller.hoveredIdx}
132
- setHoveredIdx={controller.actions.setHoveredIdx}
133
- onSelectRow={(rowKey) => controller.actions.setBrowseSelection(rowKey)}
134
- onOpenRow={controller.actions.openSelectedRow}
135
- watchlist={controller.watchlistSet}
136
- onToggleWatchlist={controller.actions.toggleWatchlist}
137
- sortPreference={controller.sortPreference}
138
- onHeaderClick={controller.actions.handleSortHeaderClick}
139
- headerScrollRef={controller.headerScrollRef}
140
- scrollRef={controller.scrollRef}
141
- syncHeaderScroll={controller.layout.syncHeaderScroll}
142
- onBodyScrollActivity={controller.layout.onBodyScrollActivity}
143
- />
144
- </box>
145
- </box>
186
+ </>
146
187
  );
147
188
 
189
+ const renderCell = useCallback((
190
+ row: PredictionListRow,
191
+ column: PredictionColumnDef,
192
+ ) => {
193
+ const watchlisted = watchlistedRowKeys.has(row.key);
194
+ const value = cellCacheRef.current.get(
195
+ `${row.key}:${column.id}`,
196
+ predictionCellVersion(row, column, watchlisted, relativeTimeBucket),
197
+ () => getPredictionColumnValue(column, row, watchlisted),
198
+ );
199
+ if (column.id === "watch") {
200
+ return {
201
+ text: value.text,
202
+ color: value.color,
203
+ onMouseDown: (event: any) => {
204
+ event.preventDefault();
205
+ event.stopPropagation?.();
206
+ controller.actions.toggleWatchlist(row);
207
+ },
208
+ };
209
+ }
210
+ return {
211
+ text: value.text,
212
+ color: value.color,
213
+ };
214
+ }, [
215
+ controller.actions.toggleWatchlist,
216
+ relativeTimeBucket,
217
+ watchlistedRowKeys,
218
+ ]);
219
+
148
220
  const detailContent =
149
221
  controller.selectedSummary && controller.selectedRow ? (
150
- <box
222
+ <Box
151
223
  flexDirection="column"
152
224
  flexGrow={1}
153
225
  width={width}
@@ -172,18 +244,44 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
172
244
  selectedRow={controller.selectedRow}
173
245
  selectedSummary={controller.selectedSummary}
174
246
  />
175
- </box>
247
+ </Box>
176
248
  ) : (
177
- <box flexGrow={1} backgroundColor={colors.panel} />
249
+ <Box flexGrow={1} backgroundColor={colors.panel} />
178
250
  );
179
251
 
180
252
  return (
181
- <PageStackView
253
+ <DataTableStackView<PredictionListRow, PredictionColumnDef>
182
254
  focused={focused}
255
+ keyboardNavigation={!controller.searchFocused}
183
256
  detailOpen={controller.detailOpen && !!controller.selectedSummary}
184
257
  onBack={controller.actions.closeDetail}
185
- rootContent={browseContent}
186
258
  detailContent={detailContent}
259
+ rootBefore={browseControls}
260
+ rootWidth={width}
261
+ rootHeight={height}
262
+ rootBackgroundColor={colors.panel}
263
+ selectedIndex={controller.selectedIndex}
264
+ onSelectIndex={(_index, row) =>
265
+ controller.actions.setBrowseSelection(row.key, {
266
+ debounceDetail: true,
267
+ })}
268
+ onActivateIndex={(_index, row) =>
269
+ controller.actions.openSelectedRow(row.key)}
270
+ columns={controller.visibleColumns}
271
+ items={controller.visibleRows}
272
+ sortColumnId={controller.sortPreference.columnId}
273
+ sortDirection={controller.sortPreference.direction}
274
+ onHeaderClick={controller.actions.handleSortHeaderClick}
275
+ headerScrollRef={controller.headerScrollRef}
276
+ scrollRef={controller.scrollRef}
277
+ getItemKey={(row) => row.key}
278
+ isSelected={(row) => controller.selectedRow?.key === row.key}
279
+ onSelect={(row) => controller.actions.setBrowseSelection(row.key)}
280
+ onActivate={(row) => controller.actions.openSelectedRow(row.key)}
281
+ virtualize
282
+ renderCell={renderCell}
283
+ emptyStateTitle="No markets matched."
284
+ emptyStateHint="Change the venue, browse tab, or search query."
187
285
  />
188
286
  );
189
287
  }
@@ -1,10 +1,18 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
- import { cleanupPredictionTest, createConfig, installPredictionMarketMocks } from "./test-helpers";
2
+ import {
3
+ cleanupPredictionTest,
4
+ installPredictionMarketMocks,
5
+ MemoryPersistence,
6
+ } from "./test-helpers";
3
7
  import { colors } from "../../theme/colors";
4
- import { predictionMarketsPlugin } from "./index";
5
8
  import { resolvePredictionKeyboardCommand } from "./keyboard";
6
9
  import { buildPredictionListRows } from "./rows";
7
10
  import { getPredictionColumnValue } from "./metrics";
11
+ import {
12
+ attachPredictionMarketsPersistence,
13
+ loadCachedPredictionResource,
14
+ PREDICTION_CACHE_POLICIES,
15
+ } from "./services/fetch";
8
16
  import {
9
17
  loadKalshiCatalog,
10
18
  loadKalshiHistory,
@@ -21,51 +29,35 @@ afterEach(async () => {
21
29
  });
22
30
 
23
31
  describe("prediction markets plugin registration and services", () => {
24
- test("exposes the pane template and search command", () => {
25
- const commands: string[] = [];
26
- const ctx = {
27
- persistence: {
28
- getState: () => null,
29
- setState: () => {},
30
- deleteState: () => {},
31
- getResource: () => null,
32
- setResource: (_kind: string, _key: string, value: unknown) => ({
33
- value,
34
- fetchedAt: Date.now(),
35
- staleAt: Date.now(),
36
- expiresAt: Date.now(),
37
- sourceKey: "test",
38
- schemaVersion: 1,
39
- provenance: null,
40
- stale: false,
41
- expired: false,
42
- }),
43
- deleteResource: () => {},
44
- },
45
- registerCommand: (command: { id: string }) => {
46
- commands.push(command.id);
47
- },
48
- resume: {
49
- getState: () => null,
50
- setState: () => {},
51
- deleteState: () => {},
52
- getPaneState: () => null,
53
- setPaneState: () => {},
54
- deletePaneState: () => {},
32
+ test("uses fresh prediction resource cache without refetching", async () => {
33
+ const persistence = new MemoryPersistence();
34
+ attachPredictionMarketsPersistence(persistence);
35
+ const cached = [
36
+ normalizePolymarketMarket({
37
+ id: "pm-cache",
38
+ question: "Will cached data be used?",
39
+ outcomes: '["Yes","No"]',
40
+ outcomePrices: '["0.7","0.3"]',
41
+ })!,
42
+ ];
43
+ persistence.setResource("catalog", "fresh", cached, {
44
+ cachePolicy: PREDICTION_CACHE_POLICIES.catalog,
45
+ sourceKey: "remote",
46
+ });
47
+ let fetchCount = 0;
48
+
49
+ const result = await loadCachedPredictionResource(
50
+ "catalog",
51
+ "fresh",
52
+ async () => {
53
+ fetchCount += 1;
54
+ return [];
55
55
  },
56
- focusPane: () => {},
57
- getConfig: () => createConfig(),
58
- } as any;
59
-
60
- predictionMarketsPlugin.setup?.(ctx);
61
-
62
- expect(predictionMarketsPlugin.toggleable).toBe(true);
63
- expect(predictionMarketsPlugin.panes?.[0]?.defaultMode).toBe("floating");
64
- expect(predictionMarketsPlugin.paneTemplates?.[0]?.shortcut?.prefix).toBe(
65
- "PM",
56
+ PREDICTION_CACHE_POLICIES.catalog,
66
57
  );
67
- expect(commands).toContain("prediction-markets-open");
68
- expect(commands).toContain("prediction-markets-search");
58
+
59
+ expect(result).toBe(cached);
60
+ expect(fetchCount).toBe(0);
69
61
  });
70
62
 
71
63
  test("normalizes venue payloads", () => {
@@ -1,5 +1,9 @@
1
1
  import type { PluginPersistence } from "../../../types/plugin";
2
- import { createThrottledFetch } from "../../../utils/throttled-fetch";
2
+ import { httpFetch } from "../../../utils/http-transport";
3
+ import { measurePerf } from "../../../utils/perf-marks";
4
+ import {
5
+ createThrottledFetch,
6
+ } from "../../../utils/throttled-fetch";
3
7
 
4
8
  const DEFAULT_SOURCE_KEY = "remote";
5
9
  const PREDICTION_FETCH = createThrottledFetch({
@@ -12,6 +16,7 @@ const PREDICTION_FETCH = createThrottledFetch({
12
16
  Accept: "application/json",
13
17
  "User-Agent": "gloomberb-prediction-markets",
14
18
  },
19
+ transport: httpFetch,
15
20
  });
16
21
 
17
22
  export const PREDICTION_CACHE_POLICIES = {
@@ -44,7 +49,15 @@ export async function fetchJson<T>(url: string): Promise<T> {
44
49
  if (!response.ok) {
45
50
  throw new Error(`Request failed (${response.status}) for ${url}`);
46
51
  }
47
- return (await response.json()) as T;
52
+ const body = await response.text();
53
+ return measurePerf(
54
+ "prediction.fetch.parse-json",
55
+ () => JSON.parse(body) as T,
56
+ {
57
+ sizeBytes: body.length,
58
+ url: summarizePredictionFetchUrl(url),
59
+ },
60
+ );
48
61
  }
49
62
 
50
63
  export function getCachedPredictionResource<T>(
@@ -81,6 +94,9 @@ export async function loadCachedPredictionResource<T>(
81
94
  const cached = predictionMarketsPersistence?.getResource<T>(kind, key, {
82
95
  sourceKey: DEFAULT_SOURCE_KEY,
83
96
  });
97
+ if (cached && cached.stale !== true && cached.staleAt > Date.now()) {
98
+ return cached.value;
99
+ }
84
100
  try {
85
101
  const nextValue = await fetcher();
86
102
  setCachedPredictionResource(kind, key, nextValue, cachePolicy);
@@ -91,17 +107,18 @@ export async function loadCachedPredictionResource<T>(
91
107
  }
92
108
  }
93
109
 
110
+ function summarizePredictionFetchUrl(url: string): string {
111
+ try {
112
+ const parsed = new URL(url);
113
+ return `${parsed.hostname}${parsed.pathname}`;
114
+ } catch {
115
+ return url.slice(0, 120);
116
+ }
117
+ }
118
+
94
119
  export function parseFloatSafe(value: unknown): number | null {
95
120
  if (typeof value === "number" && Number.isFinite(value)) return value;
96
121
  if (typeof value !== "string" || value.trim().length === 0) return null;
97
122
  const parsed = Number.parseFloat(value);
98
123
  return Number.isFinite(parsed) ? parsed : null;
99
124
  }
100
-
101
- export function parseIntegerSafe(value: unknown): number | null {
102
- if (typeof value === "number" && Number.isFinite(value))
103
- return Math.trunc(value);
104
- if (typeof value !== "string" || value.trim().length === 0) return null;
105
- const parsed = Number.parseInt(value, 10);
106
- return Number.isFinite(parsed) ? parsed : null;
107
- }
@@ -19,6 +19,7 @@ import {
19
19
  parseFloatSafe,
20
20
  PREDICTION_CACHE_POLICIES,
21
21
  } from "./fetch";
22
+ import { measurePerf } from "../../../utils/perf-marks";
22
23
 
23
24
  interface KalshiMarketRecord {
24
25
  ticker: string;
@@ -257,6 +258,24 @@ function sortKalshiMarkets(
257
258
  );
258
259
  }
259
260
 
261
+ function normalizeKalshiCatalog(
262
+ events: KalshiEventRecord[],
263
+ searchQuery: string,
264
+ categoryId: PredictionCategoryId,
265
+ ): PredictionMarketSummary[] {
266
+ return measurePerf(
267
+ "prediction.catalog.kalshi.normalize",
268
+ () => sortKalshiMarkets(
269
+ flattenKalshiEvents(events, searchQuery, categoryId),
270
+ ),
271
+ {
272
+ categoryId,
273
+ eventCount: events.length,
274
+ search: searchQuery.trim().length > 0,
275
+ },
276
+ );
277
+ }
278
+
260
279
  async function fetchKalshiCatalogEvents(
261
280
  maxPages = DEFAULT_KALSHI_EVENT_MAX_PAGES,
262
281
  ): Promise<KalshiEventRecord[]> {
@@ -363,9 +382,7 @@ export async function loadKalshiCatalog(
363
382
  categoryId === "all"
364
383
  ? await fetchKalshiCatalogEvents(maxPages)
365
384
  : await fetchKalshiCatalogEventsForCategory(categoryId, maxPages);
366
- return sortKalshiMarkets(
367
- flattenKalshiEvents(events, normalizedQuery, categoryId),
368
- );
385
+ return normalizeKalshiCatalog(events, normalizedQuery, categoryId);
369
386
  },
370
387
  PREDICTION_CACHE_POLICIES.catalog,
371
388
  );
@@ -23,6 +23,7 @@ import {
23
23
  parseFloatSafe,
24
24
  PREDICTION_CACHE_POLICIES,
25
25
  } from "./fetch";
26
+ import { measurePerf } from "../../../utils/perf-marks";
26
27
 
27
28
  interface PolymarketEventRecord {
28
29
  id: string;
@@ -334,6 +335,25 @@ function sortPolymarketMarkets(
334
335
  );
335
336
  }
336
337
 
338
+ function normalizePolymarketCatalog(
339
+ events: PolymarketEventRecord[],
340
+ searchQuery: string,
341
+ categoryId: PredictionCategoryId,
342
+ ): PredictionMarketSummary[] {
343
+ return measurePerf(
344
+ "prediction.catalog.polymarket.normalize",
345
+ () =>
346
+ sortPolymarketMarkets(
347
+ flattenPolymarketEvents(events, searchQuery, categoryId),
348
+ ),
349
+ {
350
+ categoryId,
351
+ eventCount: events.length,
352
+ search: searchQuery.trim().length > 0,
353
+ },
354
+ );
355
+ }
356
+
337
357
  function reconcilePolymarketSearchEvents(
338
358
  searchEvents: PolymarketEventRecord[],
339
359
  hydratedEvents: PolymarketEventRecord[],
@@ -369,12 +389,10 @@ export async function loadPolymarketCatalog(
369
389
  searchEvents,
370
390
  hydratedEvents,
371
391
  );
372
- return sortPolymarketMarkets(
373
- flattenPolymarketEvents(
374
- resolvedEvents,
375
- normalizedQuery,
376
- categoryId,
377
- ),
392
+ return normalizePolymarketCatalog(
393
+ resolvedEvents,
394
+ normalizedQuery,
395
+ categoryId,
378
396
  );
379
397
  }
380
398
 
@@ -388,8 +406,10 @@ export async function loadPolymarketCatalog(
388
406
  ).catch(() => []),
389
407
  ),
390
408
  );
391
- const categorized = sortPolymarketMarkets(
392
- flattenPolymarketEvents(categoryPages.flat(), "", categoryId),
409
+ const categorized = normalizePolymarketCatalog(
410
+ categoryPages.flat(),
411
+ "",
412
+ categoryId,
393
413
  );
394
414
  if (categorized.length > 0) return categorized;
395
415
  }
@@ -397,9 +417,7 @@ export async function loadPolymarketCatalog(
397
417
  const pages = await loadPolymarketCatalogPages(
398
418
  POLYMARKET_CATALOG_OFFSETS,
399
419
  );
400
- return sortPolymarketMarkets(
401
- flattenPolymarketEvents(pages, "", categoryId),
402
- );
420
+ return normalizePolymarketCatalog(pages, "", categoryId);
403
421
  },
404
422
  PREDICTION_CACHE_POLICIES.catalog,
405
423
  );
@@ -116,8 +116,6 @@ export function buildPredictionMarketsPaneSettingsDef(
116
116
  {
117
117
  key: "columnIds",
118
118
  label: "Columns",
119
- description:
120
- "Choose which columns this prediction-markets pane shows and in what order.",
121
119
  type: "ordered-multi-select",
122
120
  options: PREDICTION_COLUMN_DEFS.map((column) => ({
123
121
  value: column.id,
@@ -137,7 +135,6 @@ export function buildPredictionMarketsPaneSettingsDef(
137
135
  fields.push({
138
136
  key: "lockedVenueScope",
139
137
  label: "Locked Venue Scope",
140
- description: "Choose the venue scope to show when tabs are hidden.",
141
138
  type: "select",
142
139
  options: VENUE_SCOPE_OPTIONS,
143
140
  });
@@ -146,13 +143,11 @@ export function buildPredictionMarketsPaneSettingsDef(
146
143
  fields.push({
147
144
  key: "hideHeader",
148
145
  label: "Hide Header",
149
- description: "Hide the compact status header above the table.",
150
146
  type: "toggle",
151
147
  });
152
148
  fields.push({
153
149
  key: "defaultBrowseTab",
154
150
  label: "Default Browse Tab",
155
- description: "Choose the initial market slice for new panes.",
156
151
  type: "select",
157
152
  options: BROWSE_TAB_OPTIONS,
158
153
  });