gloomberb 0.10.4 → 0.11.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 (555) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +30 -3
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +5 -5
  5. package/src/api-client/auth.ts +34 -0
  6. package/src/api-client/data.ts +25 -0
  7. package/src/api-client/index.test.ts +81 -0
  8. package/src/api-client/index.ts +37 -0
  9. package/src/api-client/request.test.ts +30 -0
  10. package/src/api-client/request.ts +44 -20
  11. package/src/api-client/socket-health.test.ts +79 -0
  12. package/src/api-client/socket.ts +108 -5
  13. package/src/api-client/types.ts +139 -0
  14. package/src/app/global-shortcuts.test.tsx +144 -9
  15. package/src/app/global-shortcuts.ts +33 -11
  16. package/src/app/runtime/broker-import.ts +39 -5
  17. package/src/app/runtime/startup.ts +7 -1
  18. package/src/app/runtime/update.ts +26 -1
  19. package/src/app.tsx +36 -19
  20. package/src/brokers/broker-ticker-sync.ts +3 -8
  21. package/src/brokers/sync-broker-instance.test.ts +59 -0
  22. package/src/brokers/sync-broker-instance.ts +37 -13
  23. package/src/capabilities/chart-series.ts +318 -0
  24. package/src/capabilities/factories.ts +46 -0
  25. package/src/capabilities/index.ts +1 -0
  26. package/src/capabilities/registry.test.ts +135 -0
  27. package/src/capabilities/registry.ts +31 -4
  28. package/src/capabilities/types.ts +52 -0
  29. package/src/cli/commands/overview.ts +2 -2
  30. package/src/cli/desktop-pane-shot.ts +2 -0
  31. package/src/cli/entry.ts +11 -1
  32. package/src/cli/native-smoke.ts +5 -0
  33. package/src/cli/pane-functions/catalog.ts +4 -0
  34. package/src/cli/pane-functions/data.ts +6 -1
  35. package/src/cli/pane-functions/discovery.ts +76 -10
  36. package/src/cli/pane-functions/report.test.ts +76 -0
  37. package/src/cli/pane-functions/report.ts +7 -1
  38. package/src/cli/pane-functions/resolver.ts +4 -1
  39. package/src/cli/pane-functions/screenshot.test.ts +56 -3
  40. package/src/cli/pane-functions/screenshot.ts +68 -5
  41. package/src/components/chart/composite/composite-chart.test.tsx +54 -0
  42. package/src/components/chart/composite/composite-chart.tsx +92 -15
  43. package/src/components/chart/composite/downsample.test.ts +136 -0
  44. package/src/components/chart/composite/downsample.ts +208 -0
  45. package/src/components/chart/composite/panel-series.test.ts +66 -0
  46. package/src/components/chart/composite/panel-series.ts +101 -0
  47. package/src/components/chart/multi-line/renderer.ts +1 -0
  48. package/src/components/chart/native/kitty/index.test.ts +25 -3
  49. package/src/components/chart/native/kitty/support.ts +37 -3
  50. package/src/components/chart/native/use-chart-text-fallback.ts +19 -0
  51. package/src/components/chart/static/chart/surface.tsx +43 -12
  52. package/src/components/chart/static/multi-line-chart-surface.tsx +41 -5
  53. package/src/components/command-bar/commands/direct/index.ts +18 -0
  54. package/src/components/command-bar/commands/registry.ts +14 -0
  55. package/src/components/command-bar/routes/root/article-results.test.ts +83 -0
  56. package/src/components/command-bar/routes/root/article-results.ts +55 -0
  57. package/src/components/command-bar/routes/root/command-items.ts +3 -0
  58. package/src/components/command-bar/routes/root/provider-search.ts +7 -3
  59. package/src/components/command-bar/routes/root/results.ts +38 -11
  60. package/src/components/command-bar/routes/root/runtime.ts +6 -2
  61. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -1
  62. package/src/components/command-bar/routes/ticker-search/results.test.ts +34 -0
  63. package/src/components/command-bar/routes/ticker-search/results.ts +4 -6
  64. package/src/components/command-bar/routes/ticker-search/route.ts +2 -2
  65. package/src/components/command-bar/routing/list-state.ts +2 -1
  66. package/src/components/command-bar/surface/assist.test.tsx +13 -6
  67. package/src/components/command-bar/surface/index.test.tsx +65 -0
  68. package/src/components/command-bar/surface/index.tsx +35 -0
  69. package/src/components/command-bar/view-model.test.ts +43 -0
  70. package/src/components/command-bar/view-model.ts +41 -4
  71. package/src/components/command-bar/workflow/collection-actions.test.ts +94 -0
  72. package/src/components/command-bar/workflow/collection-actions.ts +14 -3
  73. package/src/components/command-bar/workflow/ops.test.ts +136 -0
  74. package/src/components/command-bar/workflow/ops.ts +71 -1
  75. package/src/components/index.ts +1 -1
  76. package/src/components/layout/header.test.tsx +35 -0
  77. package/src/components/layout/header.tsx +101 -9
  78. package/src/components/layout/pane/cycle-order.test.ts +0 -1
  79. package/src/components/layout/pane/footer/index.test.tsx +19 -19
  80. package/src/components/layout/pane/header.tsx +2 -13
  81. package/src/components/layout/shell/index.tsx +3 -0
  82. package/src/components/layout/status-bar.test.tsx +36 -1
  83. package/src/components/layout/status-bar.tsx +48 -2
  84. package/src/components/onboarding/account-step/chooser-panel.tsx +27 -50
  85. package/src/components/onboarding/account-step/form-panel.tsx +85 -29
  86. package/src/components/onboarding/account-step/index.tsx +10 -1
  87. package/src/components/onboarding/account-step/model.ts +3 -3
  88. package/src/components/onboarding/account-step/qr-panel.tsx +68 -0
  89. package/src/components/onboarding/account-step/signed-in-panel.tsx +4 -7
  90. package/src/components/onboarding/onboarding-frame.tsx +517 -0
  91. package/src/components/onboarding/onboarding-steps.tsx +0 -264
  92. package/src/components/onboarding/onboarding-wizard.test.tsx +626 -200
  93. package/src/components/onboarding/onboarding-wizard.tsx +848 -266
  94. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +81 -51
  95. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +14 -20
  96. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +8 -11
  97. package/src/components/onboarding/portfolio-step/choose-panel.tsx +18 -22
  98. package/src/components/onboarding/portfolio-step/index.tsx +3 -0
  99. package/src/components/onboarding/portfolio-step/types.ts +3 -0
  100. package/src/components/onboarding/wizard-account.ts +38 -3
  101. package/src/components/onboarding/wizard-broker-sync.ts +45 -67
  102. package/src/components/onboarding/wizard-model.ts +18 -2
  103. package/src/components/ui/data-table/opentui/index.tsx +29 -9
  104. package/src/components/ui/data-table/types.ts +2 -0
  105. package/src/components/ui/external-link.tsx +4 -2
  106. package/src/components/ui/loading.tsx +1 -1
  107. package/src/components/ui/status.tsx +76 -4
  108. package/src/components/ui/table-layout.test.ts +24 -0
  109. package/src/components/ui/table-layout.ts +90 -23
  110. package/src/components/ui/tabs.tsx +86 -4
  111. package/src/core/app-services.ts +14 -3
  112. package/src/core/connection-health.test.ts +143 -0
  113. package/src/core/connection-health.ts +295 -0
  114. package/src/core/state/app/layout-reducer.ts +34 -0
  115. package/src/core/state/app/layout.ts +21 -0
  116. package/src/core/state/app/state.ts +10 -0
  117. package/src/core/state/app/types.ts +7 -1
  118. package/src/data/config/chart-settings.ts +11 -2
  119. package/src/data/config/store/index.test.ts +83 -9
  120. package/src/data/config/store/migrations.ts +16 -0
  121. package/src/data/config/store/normalize.ts +59 -1
  122. package/src/data/fred-series.ts +8 -2
  123. package/src/i18n/es.ts +126 -0
  124. package/src/i18n/ja.ts +126 -0
  125. package/src/i18n/ko.ts +126 -0
  126. package/src/i18n/zh-cn.ts +126 -0
  127. package/src/i18n/zh-tw.ts +126 -0
  128. package/src/market-data/coordinator/chart.ts +3 -2
  129. package/src/market-data/hooks.tsx +3 -2
  130. package/src/market-data/market/status.test.ts +12 -0
  131. package/src/market-data/market/status.ts +34 -17
  132. package/src/news/aggregator.test.ts +39 -0
  133. package/src/news/aggregator.ts +78 -22
  134. package/src/news/hooks.ts +4 -0
  135. package/src/notifications/app-notifier.test.ts +28 -6
  136. package/src/notifications/app-notifier.ts +24 -5
  137. package/src/plugins/broker-sync/broker-sync.test.ts +130 -0
  138. package/src/plugins/broker-sync/native-loader.ts +10 -0
  139. package/src/plugins/broker-sync/normalize.ts +271 -0
  140. package/src/plugins/broker-sync/public.ts +122 -0
  141. package/src/plugins/broker-sync/robinhood-native.ts +367 -0
  142. package/src/plugins/broker-sync/robinhood.ts +114 -0
  143. package/src/plugins/broker-sync/simplefin-native.ts +201 -0
  144. package/src/plugins/broker-sync/simplefin.ts +70 -0
  145. package/src/plugins/builtin/account-management/footer.ts +8 -19
  146. package/src/plugins/builtin/account-management/pane.tsx +13 -8
  147. package/src/plugins/builtin/ai/pi/host.ts +6 -5
  148. package/src/plugins/builtin/ai/providers.ts +1 -1
  149. package/src/plugins/builtin/ai/screener/footer.ts +0 -7
  150. package/src/plugins/builtin/ai/screener/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/ai/screener/pane.tsx +3 -1
  152. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -0
  153. package/src/plugins/builtin/ai/screener/runner.ts +7 -3
  154. package/src/plugins/builtin/ai/workspace/pane.tsx +13 -2
  155. package/src/plugins/builtin/alerts/alert-engine.ts +13 -0
  156. package/src/plugins/builtin/alerts/constants.ts +2 -1
  157. package/src/plugins/builtin/alerts/index.tsx +67 -36
  158. package/src/plugins/builtin/alerts/pane.tsx +17 -65
  159. package/src/plugins/builtin/analytics/index.tsx +24 -11
  160. package/src/plugins/builtin/analytics/pane-model.ts +48 -14
  161. package/src/plugins/builtin/analytics/portfolio-selection.ts +19 -0
  162. package/src/plugins/builtin/analytics/view.tsx +5 -3
  163. package/src/plugins/builtin/broker-manager/detail.tsx +7 -2
  164. package/src/plugins/builtin/broker-manager/footer.ts +4 -9
  165. package/src/plugins/builtin/broker-manager/index.tsx +1 -6
  166. package/src/plugins/builtin/buildout/detail/ui.tsx +15 -4
  167. package/src/plugins/builtin/buildout/pane/header.tsx +1 -0
  168. package/src/plugins/builtin/changelog/index.tsx +60 -45
  169. package/src/plugins/builtin/chart-composer/catalog-inventory.test.ts +97 -0
  170. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +366 -0
  171. package/src/plugins/builtin/chart-composer/chart-spec.ts +21 -1
  172. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +307 -0
  173. package/src/plugins/builtin/chart-composer/editor/controller.ts +1 -1
  174. package/src/plugins/builtin/chart-composer/index.tsx +45 -5
  175. package/src/plugins/builtin/chart-composer/pane.tsx +7 -88
  176. package/src/plugins/builtin/chart-composer/presets.test.ts +48 -0
  177. package/src/plugins/builtin/chart-composer/presets.ts +69 -3
  178. package/src/plugins/builtin/chart-composer/quick-add.tsx +8 -5
  179. package/src/plugins/builtin/chart-composer/semantic.ts +18 -14
  180. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +39 -0
  181. package/src/plugins/builtin/chart-composer/series-catalog.ts +104 -3
  182. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +10 -10
  183. package/src/plugins/builtin/chart-composer/shortcuts.ts +4 -12
  184. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +230 -23
  185. package/src/plugins/builtin/chat/chat.test.tsx +22 -0
  186. package/src/plugins/builtin/chat/content/composer.tsx +6 -4
  187. package/src/plugins/builtin/chat/content/index.tsx +7 -0
  188. package/src/plugins/builtin/chat/content/snapshot.ts +9 -2
  189. package/src/plugins/builtin/chat/content/transcript.tsx +10 -0
  190. package/src/plugins/builtin/chat/controller/channel-actions.ts +11 -1
  191. package/src/plugins/builtin/chat/controller/channels.ts +13 -3
  192. package/src/plugins/builtin/chat/controller/index.ts +61 -7
  193. package/src/plugins/builtin/chat/controller/lifecycle.ts +3 -0
  194. package/src/plugins/builtin/chat/controller/message-loading.ts +5 -1
  195. package/src/plugins/builtin/chat/controller/messages.ts +3 -1
  196. package/src/plugins/builtin/chat/controller/notifications.ts +32 -11
  197. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  198. package/src/plugins/builtin/chat/controller/view.ts +1 -0
  199. package/src/plugins/builtin/chat/controller.test.ts +117 -28
  200. package/src/plugins/builtin/cloud/auth-commands.ts +18 -0
  201. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +243 -0
  202. package/src/plugins/builtin/cloud/device-signin.test.ts +217 -0
  203. package/src/plugins/builtin/cloud/device-signin.ts +240 -0
  204. package/src/plugins/builtin/cloud/plugin.tsx +7 -1
  205. package/src/plugins/builtin/cloud-tweets/table.tsx +57 -10
  206. package/src/plugins/builtin/composite-plugins.ts +39 -5
  207. package/src/plugins/builtin/congress-trades/detail.tsx +2 -4
  208. package/src/plugins/builtin/congress-trades/footer.ts +7 -16
  209. package/src/plugins/builtin/congress-trades/pane.tsx +12 -20
  210. package/src/plugins/builtin/connections/index.ts +53 -0
  211. package/src/plugins/builtin/connections/pane.test.tsx +79 -0
  212. package/src/plugins/builtin/connections/pane.tsx +258 -0
  213. package/src/plugins/builtin/correlation/index.test.tsx +2 -1
  214. package/src/plugins/builtin/correlation/index.tsx +58 -19
  215. package/src/plugins/builtin/correlation/matrix/model.ts +18 -7
  216. package/src/plugins/builtin/correlation/relationship/pane.tsx +43 -19
  217. package/src/plugins/builtin/correlation/settings.ts +1 -1
  218. package/src/plugins/builtin/credit-conditions/client.test.ts +80 -0
  219. package/src/plugins/builtin/credit-conditions/client.ts +105 -0
  220. package/src/plugins/builtin/credit-conditions/index.test.tsx +111 -0
  221. package/src/plugins/builtin/credit-conditions/index.tsx +227 -0
  222. package/src/plugins/builtin/credit-conditions/model.test.ts +43 -0
  223. package/src/plugins/builtin/credit-conditions/model.ts +64 -0
  224. package/src/plugins/builtin/debug/index.tsx +11 -6
  225. package/src/plugins/builtin/dividend-yield/client.test.ts +58 -0
  226. package/src/plugins/builtin/dividend-yield/client.ts +252 -0
  227. package/src/plugins/builtin/dividend-yield/index.tsx +61 -0
  228. package/src/plugins/builtin/dividend-yield/model.ts +83 -0
  229. package/src/plugins/builtin/dividend-yield/pane.tsx +305 -0
  230. package/src/plugins/builtin/dividend-yield/types.ts +22 -0
  231. package/src/plugins/builtin/earnings/data/cache.test.ts +10 -7
  232. package/src/plugins/builtin/earnings/data/cache.ts +23 -9
  233. package/src/plugins/builtin/earnings/index.tsx +57 -6
  234. package/src/plugins/builtin/econ/calendar-model.test.ts +37 -3
  235. package/src/plugins/builtin/econ/calendar-model.ts +32 -26
  236. package/src/plugins/builtin/econ/fred-cache.test.ts +8 -1
  237. package/src/plugins/builtin/econ/fred-series-map.ts +18 -0
  238. package/src/plugins/builtin/econ/index.test.tsx +105 -0
  239. package/src/plugins/builtin/econ/index.tsx +95 -52
  240. package/src/plugins/builtin/fear-greed/cache.test.ts +18 -3
  241. package/src/plugins/builtin/fear-greed/cache.ts +18 -6
  242. package/src/plugins/builtin/fear-greed/charts.tsx +9 -2
  243. package/src/plugins/builtin/fear-greed/format.ts +0 -9
  244. package/src/plugins/builtin/fear-greed/pane.tsx +21 -21
  245. package/src/plugins/builtin/futures/contracts.ts +100 -0
  246. package/src/plugins/builtin/futures/index.tsx +298 -0
  247. package/src/plugins/builtin/futures/model.test.ts +129 -0
  248. package/src/plugins/builtin/futures/model.ts +136 -0
  249. package/src/plugins/builtin/futures/pane.test.tsx +239 -0
  250. package/src/plugins/builtin/futures/table.test.ts +155 -0
  251. package/src/plugins/builtin/futures/table.tsx +241 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +160 -134
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +12 -1
  254. package/src/plugins/builtin/fx-matrix/pane.test.tsx +109 -0
  255. package/src/plugins/builtin/help/components.tsx +1 -30
  256. package/src/plugins/builtin/help/index.tsx +5 -25
  257. package/src/plugins/builtin/holders/pane.tsx +17 -12
  258. package/src/plugins/builtin/holders/treemap.tsx +4 -2
  259. package/src/plugins/builtin/insider/index.tsx +20 -10
  260. package/src/plugins/builtin/ipo-calendar/cache.ts +101 -0
  261. package/src/plugins/builtin/ipo-calendar/client.test.ts +31 -0
  262. package/src/plugins/builtin/ipo-calendar/client.ts +296 -0
  263. package/src/plugins/builtin/ipo-calendar/index.tsx +65 -0
  264. package/src/plugins/builtin/ipo-calendar/model.ts +145 -0
  265. package/src/plugins/builtin/ipo-calendar/pane.tsx +328 -0
  266. package/src/plugins/builtin/ipo-calendar/types.ts +19 -0
  267. package/src/plugins/builtin/kelly-sizer/index.test.tsx +3 -3
  268. package/src/plugins/builtin/kelly-sizer/model.ts +8 -1
  269. package/src/plugins/builtin/kelly-sizer/pane.tsx +33 -15
  270. package/src/plugins/builtin/kelly-sizer/view.tsx +12 -5
  271. package/src/plugins/builtin/market-halts/client.ts +114 -0
  272. package/src/plugins/builtin/market-halts/index.tsx +29 -0
  273. package/src/plugins/builtin/market-halts/model.test.ts +187 -0
  274. package/src/plugins/builtin/market-halts/model.ts +291 -0
  275. package/src/plugins/builtin/market-halts/pane.tsx +256 -0
  276. package/src/plugins/builtin/market-heatmap/data.test.ts +1 -0
  277. package/src/plugins/builtin/market-heatmap/data.ts +12 -3
  278. package/src/plugins/builtin/market-heatmap/index.tsx +131 -46
  279. package/src/plugins/builtin/market-movers/index.tsx +145 -63
  280. package/src/plugins/builtin/market-movers/model.ts +19 -3
  281. package/src/plugins/builtin/market-movers/screener.test.ts +108 -0
  282. package/src/plugins/builtin/market-movers/screener.ts +168 -10
  283. package/src/plugins/builtin/news/index.tsx +78 -99
  284. package/src/plugins/builtin/news/wire/article-search.test.ts +72 -0
  285. package/src/plugins/builtin/news/wire/article-search.ts +140 -0
  286. package/src/plugins/builtin/news/wire/breaking/pane.tsx +18 -8
  287. package/src/plugins/builtin/news/wire/categories.ts +35 -0
  288. package/src/plugins/builtin/news/wire/default-feeds.ts +47 -1
  289. package/src/plugins/builtin/news/wire/feed-config.test.ts +10 -5
  290. package/src/plugins/builtin/news/wire/index.tsx +41 -4
  291. package/src/plugins/builtin/news/wire/industry-pane.tsx +38 -19
  292. package/src/plugins/builtin/news/wire/news/detail-view.tsx +10 -5
  293. package/src/plugins/builtin/news/wire/news/footer.ts +1 -0
  294. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +15 -6
  295. package/src/plugins/builtin/news/wire/news/query-presets.ts +16 -2
  296. package/src/plugins/builtin/news/wire/news/table.test.tsx +54 -0
  297. package/src/plugins/builtin/news/wire/news/table.tsx +111 -17
  298. package/src/plugins/builtin/notes/files.test.ts +18 -0
  299. package/src/plugins/builtin/notes/files.ts +10 -4
  300. package/src/plugins/builtin/notes/index.tsx +4 -1
  301. package/src/plugins/builtin/notes/quick-notes-pane.test.tsx +182 -0
  302. package/src/plugins/builtin/notes/quick-notes-pane.tsx +74 -31
  303. package/src/plugins/builtin/notes/ticker-notes-tab.test.tsx +380 -0
  304. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +97 -40
  305. package/src/plugins/builtin/options/calc-seed.test.ts +118 -0
  306. package/src/plugins/builtin/options/calc-seed.ts +52 -0
  307. package/src/plugins/builtin/options/footer.ts +4 -1
  308. package/src/plugins/builtin/options/index.tsx +22 -1
  309. package/src/plugins/builtin/options/live-quotes.ts +8 -0
  310. package/src/plugins/builtin/options/table.ts +3 -1
  311. package/src/plugins/builtin/options/view.test.tsx +5 -1
  312. package/src/plugins/builtin/options/view.tsx +112 -17
  313. package/src/plugins/builtin/options-calculator/index.tsx +35 -0
  314. package/src/plugins/builtin/options-calculator/model.test.ts +172 -0
  315. package/src/plugins/builtin/options-calculator/model.ts +273 -0
  316. package/src/plugins/builtin/options-calculator/pane.test.tsx +138 -0
  317. package/src/plugins/builtin/options-calculator/pane.tsx +218 -0
  318. package/src/plugins/builtin/plugin-module.test.ts +16 -2
  319. package/src/plugins/builtin/plugin-module.ts +5 -1
  320. package/src/plugins/builtin/polls/client.ts +57 -0
  321. package/src/plugins/builtin/polls/hooks.ts +20 -0
  322. package/src/plugins/builtin/polls/index.tsx +35 -0
  323. package/src/plugins/builtin/polls/normalize.test.ts +230 -0
  324. package/src/plugins/builtin/polls/normalize.ts +319 -0
  325. package/src/plugins/builtin/polls/pane.tsx +722 -0
  326. package/src/plugins/builtin/polls/types.ts +77 -0
  327. package/src/plugins/builtin/portfolio-list/header.tsx +15 -4
  328. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -3
  329. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +1 -1
  330. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -0
  331. package/src/plugins/builtin/portfolio-list/summary/index.tsx +2 -1
  332. package/src/plugins/builtin/research/analyst-pane.tsx +40 -67
  333. package/src/plugins/builtin/research/corporate-actions-pane.tsx +25 -19
  334. package/src/plugins/builtin/research/index.test.ts +6 -25
  335. package/src/plugins/builtin/research/index.tsx +11 -5
  336. package/src/plugins/builtin/research/relative-valuation-pane.tsx +86 -26
  337. package/src/plugins/builtin/scanner/denied.tsx +31 -0
  338. package/src/plugins/builtin/scanner/feed.ts +94 -0
  339. package/src/plugins/builtin/scanner/flow-model.test.ts +85 -0
  340. package/src/plugins/builtin/scanner/flow-model.ts +133 -0
  341. package/src/plugins/builtin/scanner/flow-pane.tsx +197 -0
  342. package/src/plugins/builtin/scanner/hilo-bars.tsx +121 -0
  343. package/src/plugins/builtin/scanner/hilo-model.test.ts +44 -0
  344. package/src/plugins/builtin/scanner/hilo-model.ts +75 -0
  345. package/src/plugins/builtin/scanner/hilo-pane.tsx +165 -0
  346. package/src/plugins/builtin/scanner/index.ts +109 -0
  347. package/src/plugins/builtin/scanner/waiting.tsx +11 -0
  348. package/src/plugins/builtin/sec/index.tsx +11 -7
  349. package/src/plugins/builtin/sectors/index.tsx +105 -81
  350. package/src/plugins/builtin/sectors/move-bar.tsx +43 -0
  351. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  352. package/src/plugins/builtin/sectors/sector-model.ts +13 -15
  353. package/src/plugins/builtin/shared/auto-refresh.ts +51 -0
  354. package/src/plugins/builtin/shared/cloud-upgrade.ts +29 -5
  355. package/src/plugins/builtin/shared/pane-footer.ts +16 -3
  356. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +91 -0
  357. package/src/plugins/builtin/shared/screener-live-quotes.ts +134 -0
  358. package/src/plugins/builtin/shared/table-pane.ts +1 -5
  359. package/src/plugins/builtin/shared/use-quote-board.test.tsx +211 -0
  360. package/src/plugins/builtin/shared/use-quote-board.ts +195 -0
  361. package/src/plugins/builtin/short-interest/client.ts +124 -0
  362. package/src/plugins/builtin/short-interest/index.tsx +62 -0
  363. package/src/plugins/builtin/short-interest/model.ts +113 -0
  364. package/src/plugins/builtin/short-interest/pane.tsx +252 -0
  365. package/src/plugins/builtin/short-interest/types.ts +9 -0
  366. package/src/plugins/builtin/substack/article-detail.tsx +15 -1
  367. package/src/plugins/builtin/substack/pane-footer.ts +3 -22
  368. package/src/plugins/builtin/substack/pane.tsx +1 -2
  369. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  370. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  371. package/src/plugins/builtin/thirteenf/pane.tsx +6 -2
  372. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +10 -4
  373. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +67 -11
  374. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +15 -8
  375. package/src/plugins/builtin/ticker-detail/index.tsx +5 -21
  376. package/src/plugins/builtin/ticker-detail/overview/components.tsx +72 -32
  377. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -10
  378. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -1
  379. package/src/plugins/builtin/ticker-detail/quote-monitor/card.test.tsx +70 -0
  380. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +34 -26
  381. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +96 -49
  382. package/src/plugins/builtin/ticker-detail/research-tabs.ts +32 -0
  383. package/src/plugins/builtin/ticker-research-backend-plugin.ts +2 -9
  384. package/src/plugins/builtin/ticker-research-plugin.ts +6 -0
  385. package/src/plugins/builtin/treasury-auctions/cache.test.ts +173 -0
  386. package/src/plugins/builtin/treasury-auctions/cache.ts +104 -0
  387. package/src/plugins/builtin/treasury-auctions/client.test.ts +181 -0
  388. package/src/plugins/builtin/treasury-auctions/client.ts +151 -0
  389. package/src/plugins/builtin/treasury-auctions/index.tsx +90 -0
  390. package/src/plugins/builtin/treasury-auctions/model.test.ts +147 -0
  391. package/src/plugins/builtin/treasury-auctions/model.ts +188 -0
  392. package/src/plugins/builtin/treasury-auctions/pane.test.tsx +170 -0
  393. package/src/plugins/builtin/treasury-auctions/pane.tsx +417 -0
  394. package/src/plugins/builtin/treasury-auctions/types.ts +59 -0
  395. package/src/plugins/builtin/tv/pane.tsx +67 -7
  396. package/src/plugins/builtin/tv/youtube-embed.test.ts +84 -0
  397. package/src/plugins/builtin/tv/youtube-embed.ts +173 -0
  398. package/src/plugins/builtin/tv/youtube-stream.ts +27 -2
  399. package/src/plugins/builtin/volatility/client.ts +87 -0
  400. package/src/plugins/builtin/volatility/index.test.tsx +107 -0
  401. package/src/plugins/builtin/volatility/index.tsx +193 -0
  402. package/src/plugins/builtin/volatility/model.test.ts +66 -0
  403. package/src/plugins/builtin/volatility/model.ts +113 -0
  404. package/src/plugins/builtin/world-indices/footer.ts +43 -23
  405. package/src/plugins/builtin/world-indices/index.tsx +44 -93
  406. package/src/plugins/builtin/world-indices/indices.ts +11 -2
  407. package/src/plugins/builtin/world-indices/model.ts +17 -24
  408. package/src/plugins/builtin/world-indices/pane.test.tsx +116 -0
  409. package/src/plugins/builtin/world-indices/table.tsx +82 -37
  410. package/src/plugins/builtin/yield-curve/index.tsx +16 -7
  411. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +25 -0
  412. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -0
  413. package/src/plugins/catalog-backend.ts +8 -0
  414. package/src/plugins/catalog-ui.ts +8 -0
  415. package/src/plugins/event-bus.ts +1 -0
  416. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  417. package/src/plugins/ibkr/trade/tab/footer.ts +9 -23
  418. package/src/plugins/ibkr/trade/tab/index.tsx +1 -5
  419. package/src/plugins/ibkr/trading/pane-view.tsx +149 -87
  420. package/src/plugins/ibkr/trading/pane.tsx +2 -2
  421. package/src/plugins/ownership.ts +3 -0
  422. package/src/plugins/pane-manager/floating.ts +8 -2
  423. package/src/plugins/pane-manager.test.ts +13 -0
  424. package/src/plugins/prediction-markets/backend-plugin.ts +2 -0
  425. package/src/plugins/prediction-markets/capability.test.ts +281 -0
  426. package/src/plugins/prediction-markets/capability.ts +171 -0
  427. package/src/plugins/prediction-markets/chart.tsx +18 -26
  428. package/src/plugins/prediction-markets/columns.ts +3 -2
  429. package/src/plugins/prediction-markets/controller/catalog.ts +15 -12
  430. package/src/plugins/prediction-markets/controller/detail.ts +4 -0
  431. package/src/plugins/prediction-markets/controller/index.ts +0 -15
  432. package/src/plugins/prediction-markets/detail/book.tsx +3 -23
  433. package/src/plugins/prediction-markets/detail/overview.tsx +0 -18
  434. package/src/plugins/prediction-markets/detail/pane.tsx +36 -12
  435. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  436. package/src/plugins/prediction-markets/index.tsx +2 -0
  437. package/src/plugins/prediction-markets/metrics.ts +28 -18
  438. package/src/plugins/prediction-markets/navigation.ts +1 -0
  439. package/src/plugins/prediction-markets/pane.test.tsx +1 -1
  440. package/src/plugins/prediction-markets/pane.tsx +0 -1
  441. package/src/plugins/prediction-markets/services/fetch.ts +3 -2
  442. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +82 -23
  443. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +3 -1
  444. package/src/plugins/prediction-markets/services/kalshi/types.ts +14 -0
  445. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +26 -14
  446. package/src/plugins/prediction-markets/services/polymarket/detail.ts +67 -18
  447. package/src/plugins/prediction-markets/test-helpers.tsx +0 -1
  448. package/src/plugins/prediction-markets/types.ts +3 -9
  449. package/src/plugins/registry/context.ts +6 -1
  450. package/src/plugins/registry/index.test.ts +2 -0
  451. package/src/plugins/registry/index.ts +53 -3
  452. package/src/plugins/runtime/context.tsx +4 -2
  453. package/src/plugins/runtime/index.tsx +9 -0
  454. package/src/remote/controller.test.ts +17 -4
  455. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  456. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -2
  457. package/src/renderers/electrobun/bun/desktop/capability-bridge.test.ts +64 -0
  458. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +41 -7
  459. package/src/renderers/electrobun/bun/desktop/host-requests.ts +5 -2
  460. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +33 -0
  461. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +53 -6
  462. package/src/renderers/electrobun/bun/desktop/initialization.test.ts +27 -0
  463. package/src/renderers/electrobun/bun/desktop/initialization.ts +6 -1
  464. package/src/renderers/electrobun/bun/index.ts +1 -0
  465. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  466. package/src/renderers/electrobun/view/app-services.ts +25 -2
  467. package/src/renderers/electrobun/view/build-assets.ts +5 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +186 -65
  469. package/src/renderers/electrobun/view/cli-pane-shot-health.test.ts +25 -0
  470. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +31 -0
  471. package/src/renderers/electrobun/view/data-table/index.tsx +10 -11
  472. package/src/renderers/electrobun/view/data-table/row.tsx +2 -0
  473. package/src/renderers/electrobun/view/desktop/controls.test.tsx +68 -0
  474. package/src/renderers/electrobun/view/desktop/controls.tsx +25 -1
  475. package/src/renderers/electrobun/view/host/box.test.tsx +80 -0
  476. package/src/renderers/electrobun/view/host/media-surface.tsx +70 -3
  477. package/src/renderers/electrobun/view/host/tabs.tsx +97 -5
  478. package/src/renderers/electrobun/view/host/text.tsx +5 -2
  479. package/src/renderers/electrobun/view/input-host.test.tsx +140 -0
  480. package/src/renderers/electrobun/view/input-host.tsx +4 -2
  481. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +19 -1
  482. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +7 -0
  483. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +10 -0
  484. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +46 -0
  485. package/src/renderers/electrobun/view/remote/capability-invoker.ts +34 -13
  486. package/src/renderers/electrobun/view/remote/connection-health-backend.ts +19 -0
  487. package/src/renderers/electrobun/view/remote/connection-health.test.ts +44 -0
  488. package/src/renderers/electrobun/view/remote/connection-health.ts +49 -0
  489. package/src/renderers/electrobun/view/styles.css +5 -0
  490. package/src/renderers/opentui/dialog-host.tsx +279 -80
  491. package/src/renderers/opentui/host.tsx +15 -1
  492. package/src/renderers/opentui/terminal-media.test.ts +102 -0
  493. package/src/renderers/opentui/terminal-media.ts +142 -0
  494. package/src/renderers/opentui/test-utils.tsx +9 -6
  495. package/src/renderers/opentui/toast-host.tsx +136 -7
  496. package/src/renderers/opentui/ui-host.tsx +19 -3
  497. package/src/sources/provider-router/connection-health.test.ts +28 -0
  498. package/src/sources/provider-router/connection-health.ts +47 -0
  499. package/src/sources/provider-router/history.test.ts +53 -4
  500. package/src/sources/provider-router/history.ts +119 -14
  501. package/src/sources/provider-router/index.ts +12 -1
  502. package/src/sources/yahoo-finance/history.ts +2 -9
  503. package/src/sources/yahoo-finance/http.ts +1 -1
  504. package/src/sources/yahoo-finance/symbols.ts +1 -1
  505. package/src/sources/yahoo-finance/types.ts +19 -0
  506. package/src/state/app/context/index.test.tsx +101 -0
  507. package/src/state/app/context/index.tsx +10 -0
  508. package/src/state/config-save-scheduler.ts +1 -6
  509. package/src/state/persist-scheduler.test.ts +31 -0
  510. package/src/state/persist-scheduler.ts +21 -8
  511. package/src/sync/auth-transition.test.ts +36 -0
  512. package/src/sync/auth-transition.ts +22 -0
  513. package/src/sync/core-contributors.test.ts +58 -1
  514. package/src/sync/core-contributors.ts +39 -2
  515. package/src/sync/react.ts +9 -0
  516. package/src/test-support/plugin-runtime.ts +3 -0
  517. package/src/theme/font-scale.test.ts +14 -0
  518. package/src/theme/font-scale.ts +69 -0
  519. package/src/tickers/search/index.test.ts +284 -2
  520. package/src/tickers/search/index.ts +55 -18
  521. package/src/tickers/search/ranking.ts +143 -18
  522. package/src/tickers/search/types.ts +1 -0
  523. package/src/time-series/alignment.ts +15 -11
  524. package/src/time-series/field-catalog.ts +10 -0
  525. package/src/time-series/fundamentals.ts +19 -11
  526. package/src/time-series/hooks.ts +8 -2
  527. package/src/time-series/parsed-history-cache.ts +35 -0
  528. package/src/time-series/resolution.ts +10 -0
  529. package/src/time-series/resolve.test.ts +332 -3
  530. package/src/time-series/resolve.ts +225 -25
  531. package/src/time-series/spec-studies.test.ts +23 -0
  532. package/src/time-series/spec.ts +21 -2
  533. package/src/time-series/transforms.test.ts +76 -1
  534. package/src/time-series/transforms.ts +39 -8
  535. package/src/time-series/types.ts +9 -2
  536. package/src/time-series/use-chart-resolution.test.tsx +105 -1
  537. package/src/time-series/use-chart-resolution.ts +67 -13
  538. package/src/types/config.ts +30 -27
  539. package/src/types/media.ts +2 -0
  540. package/src/types/plugin.ts +8 -1
  541. package/src/ui/host.tsx +4 -0
  542. package/src/ui/pointer-drag.ts +12 -0
  543. package/src/ui/qr.test.ts +79 -0
  544. package/src/ui/qr.ts +82 -0
  545. package/src/updater/github-releases.test.ts +3 -3
  546. package/src/updater/github-releases.ts +1 -1
  547. package/src/updater.test.ts +117 -16
  548. package/src/updater.ts +32 -5
  549. package/src/utils/exchanges.ts +1 -0
  550. package/src/utils/external-url.test.ts +32 -0
  551. package/src/utils/external-url.ts +23 -0
  552. package/src/utils/sort-values.ts +31 -0
  553. package/src/utils/zoned-date-time.ts +67 -0
  554. package/src/components/onboarding/wizard-keyboard.ts +0 -306
  555. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +0 -120
@@ -11,6 +11,9 @@ import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
11
11
  import { createRemoteAssetDataClient } from "./remote/asset-data-client";
12
12
  import { RemotePersistence } from "./remote/persistence";
13
13
  import { RemoteTickerRepository } from "./remote/ticker-repository";
14
+ import { connectBackendConnectionHealth } from "./remote/connection-health-backend";
15
+ import { backendRequest, getElectrobunBackendInitSnapshot } from "./backend-rpc";
16
+ import { createCapabilityInvoker } from "./remote/capability-invoker";
14
17
 
15
18
  const servicesLog = debugLog.createLogger("services");
16
19
 
@@ -22,11 +25,20 @@ export function createElectrobunAppServices({ config }: AppServicesFactoryOption
22
25
  const tickerRepository = measurePerf("startup.services.ticker-repository", () => new RemoteTickerRepository());
23
26
  const dataProvider = measurePerf("startup.services.data-provider", () => createRemoteAssetDataClient());
24
27
  const marketData = new MarketDataCoordinator(dataProvider);
28
+ const invokeCapability = createCapabilityInvoker({
29
+ request: backendRequest,
30
+ shouldApplyDeadline: () => false,
31
+ timeoutMs: 0,
32
+ });
25
33
  const pluginRegistry = new PluginRegistry(dataProvider, tickerRepository, persistence, {
26
34
  enableCapabilityHandlers: false,
27
35
  wrapBrokerAdapter: (broker) => createRemoteBrokerAdapter(broker),
36
+ remoteCapabilityManifests: () => getElectrobunBackendInitSnapshot()?.capabilityManifests ?? [],
37
+ remoteCapabilityInvoke: (capabilityId, operationId, payload, options) => (
38
+ invokeCapability(capabilityId, operationId, payload, options)
39
+ ),
28
40
  });
29
- const newsService = new NewsService();
41
+ const newsService = new NewsService({ connectionHealth: pluginRegistry.connectionHealth });
30
42
 
31
43
  pluginRegistry.getConfigFn = () => config;
32
44
  pluginRegistry.getLayoutFn = () => config.layout;
@@ -55,6 +67,14 @@ export function createElectrobunAppServices({ config }: AppServicesFactoryOption
55
67
  measurePerf("startup.services.news-start", () => {
56
68
  newsService.start();
57
69
  });
70
+ let destroyed = false;
71
+ let disposeRemoteConnectionHealth: (() => void) | null = null;
72
+ const ready = Promise.all(pluginReadyPromises).then(() => {
73
+ if (destroyed) return;
74
+ const dispose = connectBackendConnectionHealth(pluginRegistry.connectionHealth);
75
+ if (destroyed) dispose();
76
+ else disposeRemoteConnectionHealth = dispose;
77
+ });
58
78
  servicesLog.info("create desktop web services complete", { pluginCount: plugins.length });
59
79
 
60
80
  return {
@@ -63,8 +83,11 @@ export function createElectrobunAppServices({ config }: AppServicesFactoryOption
63
83
  dataProvider,
64
84
  marketData,
65
85
  pluginRegistry,
66
- ready: Promise.all(pluginReadyPromises).then(() => {}),
86
+ ready,
67
87
  destroy() {
88
+ destroyed = true;
89
+ disposeRemoteConnectionHealth?.();
90
+ disposeRemoteConnectionHealth = null;
68
91
  setSharedMarketDataCoordinator(null);
69
92
  setSharedNewsService(null);
70
93
  newsService.stop();
@@ -24,6 +24,8 @@ const COMMON_ALIAS_RULES: AliasRule[] = [
24
24
  ["native/surface/manager", "native-stubs/chart/surface-manager.ts"],
25
25
  ["native/surface/sync", "native-stubs/chart/surface-sync.ts"],
26
26
  ["./native-loader", "plugins/ibkr/gateway/service/index.ts", "native-stubs/ibkr-native-loader.ts"],
27
+ ["./native-loader", "plugins/broker-sync/robinhood.ts", "native-stubs/broker-sync-native-loader.ts"],
28
+ ["./native-loader", "plugins/broker-sync/simplefin.ts", "native-stubs/broker-sync-native-loader.ts"],
27
29
  ];
28
30
 
29
31
  export function electrobunViewPath(...parts: string[]): string {
@@ -55,6 +57,9 @@ async function buildElectrobunViewBundle({
55
57
  minify: true,
56
58
  define: {
57
59
  "process.env.NODE_ENV": "\"production\"",
60
+ // The webview has no `process`, so the cloud endpoint override the terminal
61
+ // already reads from the environment is baked in at build time.
62
+ __GLOOMBERB_API_URL__: JSON.stringify(process.env.GLOOMBERB_API_URL ?? ""),
58
63
  },
59
64
  plugins: [
60
65
  {
@@ -1,7 +1,8 @@
1
1
  /** @jsxImportSource react */
2
2
  import { createRoot } from "react-dom/client";
3
- import { useEffect, type ComponentType, type ReactNode } from "react";
3
+ import { useEffect, type ReactNode } from "react";
4
4
  import { AppProvider, useAppDispatch } from "../../../state/app/context";
5
+ import { createCliPaneShotConnectionHealth } from "./cli-pane-shot-health";
5
6
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
6
7
  import { instrumentFromTicker } from "../../../market-data/request-types";
7
8
  import { UiHostProvider, type RendererHost } from "../../../ui/host";
@@ -11,8 +12,7 @@ import { webNativeRenderer } from "./native-renderer";
11
12
  import { WebToastHostProvider } from "./toast-host";
12
13
  import { webUiHost } from "./ui-host";
13
14
  import { getLoadablePlugins } from "../../../plugins/catalog";
14
- import { setSharedMarketDataForTests } from "../../../plugins/registry";
15
- import { PluginRenderProvider, type PluginRuntimeAccess } from "../../../plugins/runtime";
15
+ import { PluginRegistry, setSharedMarketDataForTests } from "../../../plugins/registry";
16
16
  import {
17
17
  RemoteUiRegistryProvider,
18
18
  useRemoteUiRegistry,
@@ -29,6 +29,11 @@ import {
29
29
  } from "../../../time-series/resolution";
30
30
  import type { TimeRange } from "../../../components/chart/core/types";
31
31
  import type { AppConfig } from "../../../types/config";
32
+ import type {
33
+ AppPersistencePort,
34
+ AppTickerRepositoryPort,
35
+ } from "../../../core/app-service-ports";
36
+ import type { CachedResourceRecord, ResourceCacheKey, SetResourceOptions } from "../../../data/resource-store";
32
37
  import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
33
38
  import type { OptionsChain, TickerFinancials } from "../../../types/financials";
34
39
  import type { TickerRecord } from "../../../types/ticker";
@@ -37,6 +42,8 @@ import type { PaneDef } from "../../../types/plugin";
37
42
  import { canonicalTickerKey, parsePublicTickerKey } from "../../../utils/exchanges";
38
43
  import { hydrateFredSeries, type FredSeriesCacheEntry } from "../../../data/fred-series";
39
44
  import { clipPriceHistoryToRange } from "../../../time-series/history-window";
45
+ import type { ResolvedSeries } from "../../../time-series/types";
46
+ import { chartSeriesSourceKey } from "../../../capabilities";
40
47
 
41
48
  interface CliPaneShotPayload {
42
49
  config: AppConfig;
@@ -47,6 +54,7 @@ interface CliPaneShotPayload {
47
54
  financials: Array<[string, TickerFinancials]>;
48
55
  optionsChains: Array<[string, OptionsChain]>;
49
56
  fredSeries: Array<[string, FredSeriesCacheEntry]>;
57
+ capabilitySeries: Array<[string, ResolvedSeries]>;
50
58
  paneState: Record<string, PaneRuntimeState>;
51
59
  }
52
60
 
@@ -64,7 +72,11 @@ declare global {
64
72
  // frames can capture the shell before those requests register, leaving a
65
73
  // loading chart or stale performance table in an otherwise valid PNG.
66
74
  const SHOT_READY_STABLE_FRAMES = 10;
67
- const SHOT_LOADING_TEXT_PATTERN = /\b(Loading|Rendering pane)\b/i;
75
+ // Loading is detected from an explicit marker rendered by Spinner and
76
+ // PaneStatusBody. Matching on body text instead made any pane whose real
77
+ // content contains the word "loading" (a changelog release note, a news
78
+ // headline) wait forever and time out.
79
+ const SHOT_LOADING_SELECTOR = "[data-gloom-status=\"loading\"]";
68
80
  const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
69
81
  "arrayBuffer",
70
82
  "blob",
@@ -76,6 +88,7 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
76
88
  let pendingShotWork = 0;
77
89
  let didInstallShotFetchTracker = false;
78
90
  let shotDataProvider: DataProvider | null = null;
91
+ let shotRegistry: PluginRegistry | null = null;
79
92
  const SHOT_CHART_RESOLUTION_SUPPORT = normalizeChartResolutionSupport(
80
93
  TIME_RANGE_ORDER.map((maxRange) => ({
81
94
  resolution: getPresetResolution(maxRange),
@@ -133,7 +146,23 @@ function resolveShotWork<T>(value: T): Promise<T> {
133
146
  }
134
147
 
135
148
  function isShotLoadingTextVisible(): boolean {
136
- return SHOT_LOADING_TEXT_PATTERN.test(document.body.textContent ?? "");
149
+ return document.querySelector(SHOT_LOADING_SELECTOR) !== null;
150
+ }
151
+
152
+ /**
153
+ * The payload crosses into the webview as JSON, so every `Date` arrives as a
154
+ * string while the types still claim `Date`. Anything calling `date.getTime()`
155
+ * then throws, which is what made the valuation graph preset fail while the
156
+ * price presets passed.
157
+ */
158
+ function revivePayloadDates(payload: CliPaneShotPayload): void {
159
+ for (const [, data] of payload.financials) {
160
+ const history = data.priceHistory;
161
+ if (!Array.isArray(history)) continue;
162
+ for (const point of history) {
163
+ if (!(point.date instanceof Date)) point.date = new Date(point.date as unknown as string);
164
+ }
165
+ }
137
166
  }
138
167
 
139
168
  function hasUnresolvedChartData(): boolean {
@@ -248,8 +277,17 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
248
277
  ?? financials.get(normalizeSymbol(target.symbol)))?.quote ?? null,
249
278
  })));
250
279
  },
251
- getExchangeRate() {
252
- return resolveShotWork(1);
280
+ getExchangeRate(fromCurrency: string) {
281
+ // Returning 1 for every currency used to render the FX matrix as a grid
282
+ // of 1.0000, which reads as real data. Only the identity conversion is
283
+ // known offline; anything else has to fail so the pane reports it as
284
+ // unavailable instead of inventing a parity rate.
285
+ if (normalizeSymbol(fromCurrency) === normalizeSymbol(payload.config.baseCurrency)) {
286
+ return resolveShotWork(1);
287
+ }
288
+ return trackShotWork(Promise.reject(
289
+ new Error(`No screenshot exchange rate available for ${fromCurrency}.`),
290
+ ));
253
291
  },
254
292
  getOptionsChain(ticker, exchange) {
255
293
  return trackShotWork(Promise.resolve().then(() => {
@@ -313,58 +351,147 @@ function installShotMarketData(payload: CliPaneShotPayload): void {
313
351
  setSharedMarketDataCoordinator(coordinator);
314
352
  }
315
353
 
316
- function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
317
- const resumeState = new Map<string, unknown>();
318
- function getResumeState<T = unknown>(_pluginId: string, key: string): T | null {
319
- return (resumeState.get(key) as T | undefined) ?? null;
320
- }
321
- function getConfigState<T = unknown>(pluginId: string, key: string): T | null {
322
- return (payload.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? null;
323
- }
354
+ /**
355
+ * The screenshot renderer has no database and no Electrobun backend. Plugins
356
+ * still need somewhere to read and write state while the render runs, so state
357
+ * lives in memory for the lifetime of the page and the resource cache always
358
+ * reports a miss instead of pretending something was persisted.
359
+ */
360
+ function createShotPersistence(): AppPersistencePort {
361
+ const pluginState = new Map<string, { value: unknown; schemaVersion: number; updatedAt: number }>();
362
+ const stateKey = (pluginId: string, key: string) => `${pluginId}:${key}`;
324
363
  return {
325
- getMarketData: () => shotDataProvider,
326
- getCapability: () => null,
327
- getBrokerAdapter: () => null,
328
- connectBrokerInstance: async () => {},
329
- updateBrokerInstance: async () => {},
330
- syncBrokerInstance: async () => {},
331
- removeBrokerInstance: async () => {},
332
- pinTicker: () => {},
333
- navigateTicker: () => {},
334
- selectTicker: () => {},
335
- switchTab: () => {},
336
- switchPanel: () => {},
337
- openCommandBar: () => {},
338
- showPane: () => {},
339
- createPaneFromTemplate: () => {},
340
- hidePane: () => {},
341
- focusPane: () => {},
342
- openPaneSettings: () => {},
343
- openPluginCommandWorkflow: () => {},
344
- notify: () => {},
345
- subscribeResumeState: () => () => {},
346
- getResumeState,
347
- setResumeState: (_pluginId, key, value) => {
348
- resumeState.set(key, value);
364
+ pluginState: {
365
+ get: <T,>(pluginId: string, key: string, schemaVersion = 1) => {
366
+ const record = pluginState.get(stateKey(pluginId, key));
367
+ if (!record || record.schemaVersion !== schemaVersion) return null;
368
+ return { value: record.value as T, schemaVersion: record.schemaVersion, updatedAt: record.updatedAt };
369
+ },
370
+ set: (pluginId: string, key: string, value: unknown, schemaVersion = 1) => {
371
+ pluginState.set(stateKey(pluginId, key), { value, schemaVersion, updatedAt: Date.now() });
372
+ },
373
+ delete: (pluginId: string, key: string) => {
374
+ pluginState.delete(stateKey(pluginId, key));
375
+ },
376
+ keys: (pluginId: string) => [...pluginState.keys()]
377
+ .filter((entry) => entry.startsWith(`${pluginId}:`))
378
+ .map((entry) => entry.slice(pluginId.length + 1))
379
+ .sort(),
380
+ clear: (pluginId: string) => {
381
+ for (const entry of [...pluginState.keys()]) {
382
+ if (entry.startsWith(`${pluginId}:`)) pluginState.delete(entry);
383
+ }
384
+ },
349
385
  },
350
- deleteResumeState: (_pluginId, key) => {
351
- resumeState.delete(key);
386
+ resources: {
387
+ get: () => null,
388
+ list: () => [],
389
+ set: <T,>(key: ResourceCacheKey, value: T, options: SetResourceOptions): CachedResourceRecord<T> => {
390
+ const fetchedAt = options.fetchedAt ?? Date.now();
391
+ return {
392
+ namespace: key.namespace,
393
+ kind: key.kind,
394
+ entityKey: key.entityKey,
395
+ variantKey: key.variantKey ?? "",
396
+ sourceKey: key.sourceKey ?? "",
397
+ value,
398
+ fetchedAt,
399
+ staleAt: fetchedAt + options.cachePolicy.staleMs,
400
+ expiresAt: fetchedAt + options.cachePolicy.expireMs,
401
+ schemaVersion: options.schemaVersion ?? 1,
402
+ provenance: options.provenance ?? null,
403
+ lastAccessedAt: fetchedAt,
404
+ sizeBytes: 0,
405
+ };
406
+ },
407
+ delete: () => {},
352
408
  },
353
- getConfigState,
354
- setConfigState: async () => {},
355
- setConfigStates: async () => {},
356
- deleteConfigState: async () => {},
357
- getConfigStateKeys: (pluginId) => Object.keys(payload.config.pluginConfig[pluginId] ?? {}),
409
+ sessions: {
410
+ get: () => null,
411
+ set: () => {},
412
+ delete: () => {},
413
+ },
414
+ close() {},
358
415
  };
359
416
  }
360
417
 
361
- function findPaneDef(paneId: string): { pluginId: string; pane: PaneDef } | null {
418
+ function createShotTickerRepository(tickers: TickerRecord[]): AppTickerRepositoryPort {
419
+ const bySymbol = new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker]));
420
+ return {
421
+ loadAllTickers: async () => [...bySymbol.values()],
422
+ loadTicker: async (symbol) => bySymbol.get(normalizeSymbol(symbol)) ?? bySymbol.get(symbol) ?? null,
423
+ saveTicker: async () => {},
424
+ createTicker: async (metadata) => ({ metadata }),
425
+ deleteTicker: async () => {},
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Screenshots render the same tree the desktop app renders, so they need the
431
+ * same registry: plugin-contributed tabs, slots, shortcuts, context menus and
432
+ * setup-registered panes all resolve through it. Only the capability surface is
433
+ * replaced, because the offline render can answer chart series from the payload
434
+ * but cannot reach a backend.
435
+ */
436
+ async function installShotPluginRegistry(payload: CliPaneShotPayload): Promise<PluginRegistry> {
437
+ const capabilitySeries = new Map(payload.capabilitySeries ?? []);
438
+ const capabilityIds = [...new Set(payload.config.layout.instances.flatMap((instance) => {
439
+ const chartSpec = instance.settings?.chartSpec;
440
+ if (!chartSpec || typeof chartSpec !== "object" || !Array.isArray((chartSpec as any).series)) return [];
441
+ return (chartSpec as any).series.flatMap((series: any) => (
442
+ series?.source?.kind === "capability" && typeof series.source.capabilityId === "string"
443
+ ? [series.source.capabilityId]
444
+ : []
445
+ ));
446
+ }))];
447
+
448
+ const registry = new PluginRegistry(
449
+ shotDataProvider!,
450
+ createShotTickerRepository(payload.tickers),
451
+ createShotPersistence(),
452
+ {
453
+ enableCapabilityHandlers: false,
454
+ connectionHealth: createCliPaneShotConnectionHealth(),
455
+ remoteCapabilityManifests: () => capabilityIds.map((id) => ({
456
+ id,
457
+ kind: "chart-series",
458
+ name: id,
459
+ operations: [{ id: "resolve", kind: "query", rendererSafe: true }],
460
+ })),
461
+ remoteCapabilityInvoke: async <T,>(capabilityId: string, operationId: string, input: unknown) => {
462
+ if (operationId !== "resolve" || !input || typeof input !== "object") {
463
+ throw new Error(`Screenshot capability operation ${capabilityId}.${operationId} is unavailable.`);
464
+ }
465
+ const request = input as { seriesId?: string };
466
+ const value = capabilitySeries.get(chartSeriesSourceKey({
467
+ kind: "capability",
468
+ capabilityId,
469
+ seriesId: request.seriesId ?? "",
470
+ }));
471
+ if (!value) throw new Error(`No screenshot chart series data is available for ${request.seriesId ?? capabilityId}.`);
472
+ return value as T;
473
+ },
474
+ },
475
+ );
476
+ registry.getConfigFn = () => payload.config;
477
+ registry.getLayoutFn = () => payload.config.layout;
478
+ registry.getPaneRuntimeStateFn = (paneId) => payload.paneState[paneId] ?? null;
479
+
362
480
  for (const plugin of getLoadablePlugins()) {
363
- for (const pane of plugin.panes ?? []) {
364
- if (pane.id === paneId) return { pluginId: plugin.id, pane };
481
+ try {
482
+ await registry.register(plugin);
483
+ } catch (error) {
484
+ // One failing plugin must not cost the screenshot every other pane.
485
+ console.error(`[shot] Plugin ${plugin.id} failed to register:`, error);
365
486
  }
366
487
  }
367
- return null;
488
+ shotRegistry = registry;
489
+ return registry;
490
+ }
491
+
492
+ function findPaneDef(paneId: string): { pluginId: string; pane: PaneDef } | null {
493
+ const pane = shotRegistry?.panes.get(paneId);
494
+ return pane ? { pluginId: shotRegistry?.getPanePluginId(paneId) ?? "", pane } : null;
368
495
  }
369
496
 
370
497
  function HydratePayload({
@@ -413,16 +540,8 @@ function ShotPane({ payload }: { payload: CliPaneShotPayload }) {
413
540
  const found = findPaneDef(instance.paneId);
414
541
  if (!found) throw new Error(`Pane ${instance.paneId} is not registered in the desktop renderer.`);
415
542
 
416
- const runtime = createRuntime(payload);
417
- const PaneComponent = found.pane.component as ComponentType<any>;
418
- const pane: PaneDef = {
419
- ...found.pane,
420
- component: (props) => (
421
- <PluginRenderProvider pluginId={found.pluginId} runtime={runtime}>
422
- <PaneComponent {...props} />
423
- </PluginRenderProvider>
424
- ),
425
- };
543
+ // The registry already bound its runtime to this pane component.
544
+ const pane = found.pane;
426
545
  const titleState = {
427
546
  config: payload.config,
428
547
  paneState: payload.paneState,
@@ -462,12 +581,16 @@ function ShotPane({ payload }: { payload: CliPaneShotPayload }) {
462
581
  );
463
582
  }
464
583
 
465
- function render() {
584
+ async function render() {
466
585
  const payload = window.__GLOOM_CLI_SHOT_PAYLOAD__;
467
586
  if (!payload) throw new Error("Missing CLI pane screenshot payload.");
587
+ revivePayloadDates(payload);
468
588
  installShotFetchTracker();
469
589
  hydrateFredSeries(payload.fredSeries ?? []);
470
590
  installShotMarketData(payload);
591
+ // Panes contributed from an async setup() only exist once every plugin has
592
+ // finished registering, so the tree cannot mount before that resolves.
593
+ await installShotPluginRegistry(payload);
471
594
 
472
595
  const rootElement = document.getElementById("root");
473
596
  if (!rootElement) throw new Error("Missing root element.");
@@ -498,9 +621,7 @@ function render() {
498
621
  );
499
622
  }
500
623
 
501
- try {
502
- render();
503
- } catch (error) {
624
+ render().catch((error) => {
504
625
  window.__GLOOM_CLI_SHOT_ERROR__ = error instanceof Error ? error.stack ?? error.message : String(error);
505
626
  throw error;
506
- }
627
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createCliPaneShotConnectionHealth } from "./cli-pane-shot-health";
3
+
4
+ describe("CLI pane shot connection health", () => {
5
+ test("seeds stable request, socket, and error rows", () => {
6
+ const sources = createCliPaneShotConnectionHealth(1_000).getSnapshot().sources;
7
+
8
+ expect(sources.map((source) => source.id)).toEqual([
9
+ "gloom-cloud-http",
10
+ "gloom-cloud-socket",
11
+ "gloom-cloud-fred",
12
+ "asset-data.yahoo",
13
+ ]);
14
+ expect(sources.map((source) => ({
15
+ status: source.status,
16
+ operation: source.lastOperation,
17
+ latency: source.lastLatencyMs,
18
+ }))).toEqual([
19
+ { status: "connected", operation: "GET /market/quotes", latency: 84 },
20
+ { status: "connected", operation: null, latency: null },
21
+ { status: "idle", operation: null, latency: null },
22
+ { status: "error", operation: "getPriceHistory", latency: 240 },
23
+ ]);
24
+ });
25
+ });
@@ -0,0 +1,31 @@
1
+ import {
2
+ ConnectionHealthRegistry,
3
+ GLOOM_CLOUD_HTTP_CONNECTION_ID,
4
+ GLOOM_CLOUD_SOCKET_CONNECTION_ID,
5
+ registerGloomCloudConnectionSources,
6
+ } from "../../../core/connection-health";
7
+
8
+ export function createCliPaneShotConnectionHealth(now = Date.now()): ConnectionHealthRegistry {
9
+ const health = new ConnectionHealthRegistry({ now: () => now });
10
+ registerGloomCloudConnectionSources(health);
11
+ health.reportRequest(GLOOM_CLOUD_HTTP_CONNECTION_ID, {
12
+ operation: "GET /market/quotes",
13
+ success: true,
14
+ latencyMs: 84,
15
+ });
16
+ health.reportSocketState(GLOOM_CLOUD_SOCKET_CONNECTION_ID, "open", "api.gloom.sh/cloud/ws");
17
+ health.registerSource({
18
+ id: "asset-data.yahoo",
19
+ name: "Yahoo Finance",
20
+ kind: "asset-data",
21
+ ownerId: "market-data",
22
+ priority: 20,
23
+ });
24
+ health.reportRequest("asset-data.yahoo", {
25
+ operation: "getPriceHistory",
26
+ success: false,
27
+ latencyMs: 240,
28
+ error: "Rate limited",
29
+ });
30
+ return health;
31
+ }
@@ -199,16 +199,12 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
199
199
  scheduleVisibleRangeMeasure();
200
200
  }, [items.length, scheduleVisibleRangeMeasure, visibleRangeKey]);
201
201
 
202
+ // Rows sit on a whole-cell grid, so the offset is computed in rows and only
203
+ // then converted to pixels. Letting the virtualizer scroll by pixels landed
204
+ // mid-row and clipped the first and last visible rows into slivers.
202
205
  useEffect(() => {
203
206
  if (scrollToIndex == null || items.length === 0) return;
204
207
  const targetIndex = Math.max(0, Math.min(scrollToIndex, items.length - 1));
205
- if (virtualize) {
206
- rowVirtualizer.scrollToIndex(targetIndex, {
207
- align: scrollToIndexAlign === "center" ? "center" : "auto",
208
- });
209
- scheduleVisibleRangeMeasure();
210
- return;
211
- }
212
208
  const element = bodyElementRef.current;
213
209
  if (!element) return;
214
210
  const viewportRows = Math.max(1, Math.floor(element.clientHeight / WEB_CELL_HEIGHT) - 1);
@@ -227,12 +223,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
227
223
  scheduleVisibleRangeMeasure();
228
224
  }, [
229
225
  items.length,
230
- rowVirtualizer,
231
226
  scrollToIndex,
232
227
  scrollToIndexAlign,
233
228
  scrollToIndexVersion,
234
229
  scheduleVisibleRangeMeasure,
235
- virtualize,
236
230
  ]);
237
231
 
238
232
  useScrollBoxHandle(
@@ -276,6 +270,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
276
270
  overflowX: horizontalScrollEnabled ? "auto" : "hidden",
277
271
  overflowY: "auto",
278
272
  backgroundColor: CSS_BG,
273
+ // Trim the viewport to whole rows so the bottom row is never a sliver.
274
+ // Browsers without CSS round() drop this and keep the previous behavior.
275
+ maxHeight: "round(down, 100%, var(--cell-h))",
279
276
  };
280
277
 
281
278
  return (
@@ -333,11 +330,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
333
330
  lineHeight: "var(--cell-h)",
334
331
  }}
335
332
  >
336
- <div style={cellTextStyle(CSS_TEXT_BRIGHT, TextAttributes.BOLD)}>
333
+ {/* cellTextStyle is inline-block for real cells, so the title and
334
+ hint would share one line and read as a single run-on string. */}
335
+ <div style={{ ...cellTextStyle(CSS_TEXT_BRIGHT, TextAttributes.BOLD), display: "block" }}>
337
336
  {emptyStateTitle}
338
337
  </div>
339
338
  {emptyStateHint ? (
340
- <div style={cellTextStyle(CSS_TEXT_DIM, TextAttributes.NONE)}>
339
+ <div style={{ ...cellTextStyle(CSS_TEXT_DIM, TextAttributes.NONE), display: "block" }}>
341
340
  {emptyStateHint}
342
341
  </div>
343
342
  ) : null}
@@ -208,10 +208,12 @@ function WebDataTableRowInner<
208
208
  style={{
209
209
  ...baseRowStyle,
210
210
  backgroundColor: sectionHeader.backgroundColor ?? CSS_BG,
211
+ cursor: sectionHeader.onMouseDown ? "pointer" : undefined,
211
212
  }}
212
213
  onMouseDown={(event) => {
213
214
  focusPane();
214
215
  onTableMouseDown?.(event);
216
+ sectionHeader.onMouseDown?.(event);
215
217
  event.preventDefault();
216
218
  }}
217
219
  >
@@ -0,0 +1,68 @@
1
+ /** @jsxImportSource react */
2
+ import { Window } from "happy-dom";
3
+
4
+ const testWindow = new Window({ url: "http://localhost" });
5
+ Object.assign(globalThis, {
6
+ IS_REACT_ACT_ENVIRONMENT: true,
7
+ window: testWindow,
8
+ document: testWindow.document,
9
+ navigator: testWindow.navigator,
10
+ KeyboardEvent: testWindow.KeyboardEvent,
11
+ HTMLElement: testWindow.HTMLElement,
12
+ Node: testWindow.Node,
13
+ });
14
+
15
+ import { expect, test } from "bun:test";
16
+ import { act } from "react";
17
+ import { createRoot } from "react-dom/client";
18
+ import { UiHostProvider, type RendererHost, type UiHost } from "../../../../ui";
19
+ import { WebBox } from "../host/box";
20
+ import { WebText } from "../host/text";
21
+ import { WebSegmentedControl } from "./controls";
22
+
23
+ const renderer: RendererHost = {
24
+ requestExit() {},
25
+ async openExternal() {},
26
+ async copyText() {},
27
+ async readText() { return ""; },
28
+ notify() {},
29
+ };
30
+
31
+ const ui = {
32
+ kind: "desktop-web",
33
+ capabilities: { cellWidthPx: 8, fractionalViewport: true },
34
+ Box: WebBox,
35
+ Text: WebText,
36
+ } as unknown as UiHost;
37
+
38
+ test("desktop segmented controls expose radio semantics and keyboard selection", async () => {
39
+ const selected: string[] = [];
40
+ const container = testWindow.document.createElement("div");
41
+ testWindow.document.body.appendChild(container);
42
+ const root = createRoot(container as unknown as HTMLElement);
43
+ await act(async () => {
44
+ root.render(
45
+ <UiHostProvider ui={ui} renderer={renderer}>
46
+ <WebSegmentedControl
47
+ options={[{ label: "Call", value: "call" }, { label: "Put", value: "put" }]}
48
+ value="call"
49
+ onChange={(value) => selected.push(value)}
50
+ />
51
+ </UiHostProvider>,
52
+ );
53
+ });
54
+
55
+ const group = container.querySelector('[role="radiogroup"]');
56
+ const radios = [...container.querySelectorAll('[role="radio"]')] as unknown as HTMLElement[];
57
+ expect(group).not.toBeNull();
58
+ expect(radios.map((radio) => radio.getAttribute("aria-checked"))).toEqual(["true", "false"]);
59
+
60
+ await act(async () => {
61
+ radios[0]!.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
62
+ radios[1]!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
63
+ });
64
+ expect(selected).toEqual(["put", "put"]);
65
+
66
+ await act(async () => root.unmount());
67
+ container.remove();
68
+ });