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
@@ -0,0 +1,241 @@
1
+ import type { DataTableCell, DataTableColumn } from "../../../components";
2
+ import { marketStateColor, marketStateLabel } from "../../../market-data/market/status";
3
+ import { colors, priceColor } from "../../../theme/colors";
4
+ import type { Quote } from "../../../types/financials";
5
+ import { TextAttributes } from "../../../ui";
6
+ import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
7
+ import { marketStatusDot, type BoardQuoteMap } from "../shared/use-quote-board";
8
+ import { formatQuoteTime } from "../world-indices/table";
9
+ import { tickDecimals, type FuturesContract } from "./contracts";
10
+ import type { FuturesColumnId, FuturesTableRow } from "./model";
11
+
12
+ export type FuturesColumn = DataTableColumn & { id: FuturesColumnId };
13
+
14
+ export interface FuturesColumnDef {
15
+ id: FuturesColumnId;
16
+ label: string;
17
+ description: string;
18
+ }
19
+
20
+ export const FUTURES_COLUMN_DEFS: readonly FuturesColumnDef[] = [
21
+ { id: "status", label: "Session", description: "Live trading session indicator." },
22
+ { id: "code", label: "Symbol", description: "Exchange contract code." },
23
+ { id: "name", label: "Contract", description: "Contract name." },
24
+ { id: "price", label: "Last", description: "Last traded price." },
25
+ { id: "change", label: "Change", description: "Change on the session." },
26
+ { id: "changePercent", label: "Change %", description: "Percent change on the session." },
27
+ { id: "volume", label: "Volume", description: "Contracts traded on the session." },
28
+ { id: "prevClose", label: "Prev close", description: "Previous session close." },
29
+ { id: "time", label: "Time", description: "Local time of the last quote." },
30
+ ];
31
+
32
+ const DEFAULT_FUTURES_COLUMN_IDS = FUTURES_COLUMN_DEFS.map((column) => column.id);
33
+
34
+ /**
35
+ * Extra columns only earn their width once the board is wide enough to keep a
36
+ * readable contract name; a narrow pane drops them instead of clipping.
37
+ */
38
+ const COLUMN_MIN_PANE_WIDTH: Partial<Record<FuturesColumnId, number>> = {
39
+ volume: 92,
40
+ prevClose: 104,
41
+ time: 114,
42
+ };
43
+ const SESSION_TEXT_MIN_WIDTH = 100;
44
+
45
+ const COLUMN_WIDTHS: Record<Exclude<FuturesColumnId, "name">, number> = {
46
+ status: 1,
47
+ code: 5,
48
+ price: 12,
49
+ change: 10,
50
+ changePercent: 9,
51
+ volume: 9,
52
+ prevClose: 12,
53
+ // 5-char 24h time in an 8-wide column: the shared table's floating-pane width
54
+ // accounting runs a few cells long, and the slack keeps the value intact.
55
+ time: 8,
56
+ };
57
+
58
+ /** A colored dot needs a legend; the session word does not, so wide boards spell it out. */
59
+ export function usesSessionText(width: number): boolean {
60
+ return width >= SESSION_TEXT_MIN_WIDTH;
61
+ }
62
+
63
+ function columnWidth(id: Exclude<FuturesColumnId, "name">, paneWidth: number): number {
64
+ if (id === "status") return usesSessionText(paneWidth) ? 9 : 1;
65
+ return COLUMN_WIDTHS[id];
66
+ }
67
+
68
+ export function createFuturesColumns(width: number, visibleIds?: readonly string[]): FuturesColumn[] {
69
+ const ids = resolveFuturesColumnIds(visibleIds)
70
+ .filter((id) => width >= (COLUMN_MIN_PANE_WIDTH[id] ?? 0));
71
+ const fixed = ids
72
+ .filter((id): id is Exclude<FuturesColumnId, "name"> => id !== "name")
73
+ .reduce((total, id) => total + columnWidth(id, width), 0);
74
+ // Leaves room for the pane border and scrollbar gutter; the name column grows
75
+ // back into any slack because it is the flexible one.
76
+ const nameWidth = Math.max(10, width - 6 - ids.length - fixed);
77
+
78
+ return ids.map((id) => {
79
+ const label = id === "status" && usesSessionText(width) ? "SESSION" : FUTURES_HEADER_LABELS[id];
80
+ // The leftover width lands on the contract name rather than being spread
81
+ // across the number columns, which is what left a dead zone after it.
82
+ if (id === "name") return { id, label, width: nameWidth, align: "left", flexGrow: 1 };
83
+ return {
84
+ id,
85
+ label,
86
+ width: columnWidth(id, width),
87
+ // TIME is left-aligned like the text columns: the shared table trims a few
88
+ // cells off the right edge of a floating pane, and a right-aligned value
89
+ // there would lose digits.
90
+ align: id === "code" || id === "status" || id === "time" ? "left" : "right",
91
+ };
92
+ });
93
+ }
94
+
95
+ const FUTURES_HEADER_LABELS: Record<FuturesColumnId, string> = {
96
+ status: "",
97
+ code: "SYM",
98
+ name: "CONTRACT",
99
+ price: "LAST",
100
+ change: "CHG",
101
+ changePercent: "CHG%",
102
+ volume: "VOL",
103
+ prevClose: "PREV",
104
+ time: "TIME",
105
+ };
106
+
107
+ /** Falls back to the full column set when the saved selection is empty or unknown. */
108
+ export function resolveFuturesColumnIds(visibleIds?: readonly string[]): FuturesColumnId[] {
109
+ const resolved = (visibleIds ?? [])
110
+ .filter((id): id is FuturesColumnId => DEFAULT_FUTURES_COLUMN_IDS.includes(id as FuturesColumnId));
111
+ return resolved.length > 0 ? resolved : [...DEFAULT_FUTURES_COLUMN_IDS];
112
+ }
113
+
114
+ /**
115
+ * Futures are not quoted in dollars the way equities are: grains come back in
116
+ * US cents (`USX`), FX contracts run to six decimals, and Treasury contracts
117
+ * trade in fractions of a 32nd. Scale precision to the contract instead of
118
+ * rendering everything as a currency amount.
119
+ *
120
+ * The contract's own tick wins when the catalog knows it, so silver keeps its
121
+ * $0.005 increments instead of being rounded into two decimals with gold. The
122
+ * magnitude fallback only covers contracts without a declared tick.
123
+ *
124
+ * ponytail: rates render as decimals, not the 32nds tick notation traders
125
+ * quote (108'17). Add a tick formatter if rates users ask for it.
126
+ */
127
+ function priceDecimals(price: number, contract: FuturesContract): number {
128
+ if (contract.tick) return tickDecimals(contract.tick);
129
+ if (contract.sector === "rates") return 4;
130
+ const magnitude = Math.abs(price);
131
+ if (magnitude >= 10) return 2;
132
+ if (magnitude >= 1) return 4;
133
+ return 6;
134
+ }
135
+
136
+ /**
137
+ * Trailing zeros are kept: a EUR contract at 1.1600 has to line up with the
138
+ * 1.3544 pound contract beside it, and with its own "+0.0002" change.
139
+ */
140
+ function formatContractPrice(quote: Quote, contract: FuturesContract): string {
141
+ if (!Number.isFinite(quote.price)) return "—";
142
+ const text = formatNumber(quote.price, priceDecimals(quote.price, contract));
143
+ return quote.currency === "USX" ? `${text}c` : text;
144
+ }
145
+
146
+ /**
147
+ * The session change is scaled to the contract's price, not to its own
148
+ * magnitude, otherwise a four-tick move on an index future renders with six
149
+ * decimals next to a price showing two.
150
+ */
151
+ function formatContractChange(quote: Quote, contract: FuturesContract): string {
152
+ if (!Number.isFinite(quote.change)) return "—";
153
+ const decimals = contract.tick || Number.isFinite(quote.price)
154
+ ? priceDecimals(quote.price, contract)
155
+ : 2;
156
+ const text = formatNumber(Math.abs(quote.change), decimals);
157
+ return `${quote.change >= 0 ? "+" : "-"}${text}`;
158
+ }
159
+
160
+ export function renderFuturesCell(
161
+ row: FuturesTableRow,
162
+ column: FuturesColumn,
163
+ rowState: { selected: boolean },
164
+ quotes: BoardQuoteMap,
165
+ options?: { sessionText?: boolean },
166
+ ): DataTableCell {
167
+ if (row.type === "header") return { text: "" };
168
+
169
+ const { contract } = row;
170
+ const state = quotes.get(contract.symbol);
171
+ const quote = state?.quote;
172
+ const selectedColor = rowState.selected ? colors.selectedText : undefined;
173
+ const dimmed = rowState.selected ? colors.selectedText : colors.textDim;
174
+ // One row must not mix a loading marker with a no-data marker.
175
+ const loadingCell = !quote && (state?.loading ?? true);
176
+
177
+ switch (column.id) {
178
+ case "status": {
179
+ if (loadingCell) return { text: "", color: dimmed };
180
+ if (options?.sessionText) {
181
+ const marketState = quote?.marketState;
182
+ return {
183
+ text: marketState ? marketStateLabel(marketState) : "—",
184
+ color: rowState.selected
185
+ ? colors.selectedText
186
+ : marketState ? marketStateColor(marketState) : colors.textDim,
187
+ };
188
+ }
189
+ const dot = marketStatusDot(quote?.marketState);
190
+ return { text: dot.char, color: rowState.selected ? colors.selectedText : dot.color };
191
+ }
192
+ case "code":
193
+ return {
194
+ text: contract.code,
195
+ color: selectedColor ?? colors.textBright,
196
+ attributes: TextAttributes.BOLD,
197
+ };
198
+ case "name":
199
+ return { text: contract.name, color: selectedColor };
200
+ case "price":
201
+ if (loadingCell) return { text: "…", color: dimmed };
202
+ if (!quote) return { text: "—", color: dimmed };
203
+ // A retained quote still beats a dash; dim it so stale is visible.
204
+ return {
205
+ text: formatContractPrice(quote, contract),
206
+ color: state?.stale ? dimmed : selectedColor,
207
+ };
208
+ case "change":
209
+ if (loadingCell) return { text: "…", color: dimmed };
210
+ if (!quote || !Number.isFinite(quote.change)) return { text: "—", color: dimmed };
211
+ return {
212
+ text: formatContractChange(quote, contract),
213
+ color: selectedColor ?? priceColor(quote.change),
214
+ };
215
+ case "changePercent":
216
+ if (loadingCell) return { text: "…", color: dimmed };
217
+ if (!quote || !Number.isFinite(quote.changePercent)) return { text: "—", color: dimmed };
218
+ return {
219
+ text: formatPercentRaw(quote.changePercent),
220
+ color: selectedColor ?? priceColor(quote.changePercent),
221
+ };
222
+ case "volume":
223
+ if (loadingCell) return { text: "…", color: dimmed };
224
+ if (!quote || quote.volume == null || !Number.isFinite(quote.volume)) {
225
+ return { text: "—", color: dimmed };
226
+ }
227
+ return { text: formatCompact(quote.volume), color: selectedColor ?? colors.textDim };
228
+ case "prevClose":
229
+ if (loadingCell) return { text: "…", color: dimmed };
230
+ if (!quote || quote.previousClose == null || !Number.isFinite(quote.previousClose)) {
231
+ return { text: "—", color: dimmed };
232
+ }
233
+ return {
234
+ text: formatContractPrice({ ...quote, price: quote.previousClose }, contract),
235
+ color: selectedColor ?? colors.textDim,
236
+ };
237
+ case "time":
238
+ if (loadingCell) return { text: "…", color: dimmed };
239
+ return { text: formatQuoteTime(quote?.lastUpdated), color: dimmed };
240
+ }
241
+ }
@@ -1,167 +1,193 @@
1
- import { Box, ScrollBox, Text } from "../../../ui";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { TextAttributes } from "../../../ui";
4
- import { useShortcut } from "../../../react/input";
5
- import { usePaneFooter } from "../../../components";
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import {
3
+ DataTableView,
4
+ usePaneFooter,
5
+ type DataTableCell,
6
+ type DataTableColumn,
7
+ type DataTableKeyEvent,
8
+ type PaneFooterSegment,
9
+ } from "../../../components";
10
+ import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
11
+ import { useFxRatesMap } from "../../../market-data/hooks";
12
+ import { usePaneSettingValue } from "../../../state/app/context";
13
+ import { colors } from "../../../theme/colors";
6
14
  import type { PaneProps } from "../../../types/plugin";
7
- import type { PluginModule } from "../plugin-module";
8
- import { colors, blendHex } from "../../../theme/colors";
15
+ import { TextAttributes } from "../../../ui";
16
+ import { isPlainKey } from "../../../utils/keyboard";
9
17
  import { useAssetData } from "../../runtime";
10
- import { MAJOR_CURRENCIES, formatRate, type MajorCurrency } from "./pairs";
11
-
12
- const REFRESH_INTERVAL_MS = 60_000;
18
+ import type { PluginModule } from "../plugin-module";
19
+ import { useAutoRefresh, useUpdatedAgo } from "../shared/auto-refresh";
20
+ import { MAJOR_CURRENCIES, formatRate, resolveCurrencies, type MajorCurrency } from "./pairs";
21
+
22
+ const FX_MATRIX_PANE_ID = "fx-matrix";
23
+ /** Stable identity: a fresh literal here would reload the board every render. */
24
+ const NO_SAVED_CURRENCIES: string[] = [];
25
+ const BASE_COLUMN_WIDTH = 5;
26
+ const RATE_COLUMN_WIDTH = 10;
27
+
28
+ interface FxStatus {
29
+ loading: number;
30
+ unavailable: number;
31
+ latestTs: number;
32
+ }
13
33
 
14
- function formatAge(ms: number): string {
15
- const secs = Math.floor(ms / 1000);
16
- if (secs < 60) return `${secs}s ago`;
17
- const mins = Math.floor(secs / 60);
18
- if (mins < 60) return `${mins}m ago`;
19
- return `${Math.floor(mins / 60)}h ago`;
34
+ /**
35
+ * Per-currency load state for the footer and the cell placeholders. The rates
36
+ * map above already subscribes this pane to the same coordinator keys, so
37
+ * reading the entries here re-renders with it.
38
+ */
39
+ function readFxStatus(currencies: readonly MajorCurrency[], rates: Map<string, number>): FxStatus {
40
+ const coordinator = getSharedMarketDataCoordinator();
41
+ let loading = 0;
42
+ let unavailable = 0;
43
+ let latestTs = 0;
44
+ for (const currency of currencies) {
45
+ if (currency === "USD") continue;
46
+ const entry = coordinator?.getFxEntry(currency) ?? null;
47
+ if (entry?.phase === "loading") loading += 1;
48
+ else if (!rates.has(currency)) unavailable += 1;
49
+ latestTs = Math.max(latestTs, entry?.fetchedAt ?? 0);
50
+ }
51
+ return { loading, unavailable, latestTs };
20
52
  }
21
53
 
22
54
  function FxMatrixPane({ focused, width, height }: PaneProps) {
23
55
  const dataProvider = useAssetData();
24
- const [rates, setRates] = useState<Map<MajorCurrency, number>>(new Map());
25
- const [loading, setLoading] = useState(true);
26
- const [lastRefreshed, setLastRefreshed] = useState<number | null>(null);
27
- const [now, setNow] = useState(Date.now());
28
- const fetchGenRef = useRef(0);
29
-
30
- const fetchRates = useCallback(async () => {
31
- if (!dataProvider) return;
32
-
33
- fetchGenRef.current += 1;
34
- const gen = fetchGenRef.current;
35
-
36
- try {
37
- const results = await Promise.allSettled(
38
- MAJOR_CURRENCIES.map(async (currency) => {
39
- if (currency === "USD") return { currency, rate: 1 };
40
- const rate = await dataProvider.getExchangeRate(currency);
41
- return { currency, rate };
42
- }),
43
- );
44
-
45
- if (fetchGenRef.current !== gen) return;
46
-
47
- const newRates = new Map<MajorCurrency, number>();
48
- for (const result of results) {
49
- if (result.status === "fulfilled") {
50
- newRates.set(result.value.currency as MajorCurrency, result.value.rate);
51
- }
52
- }
53
-
54
- setRates(newRates);
55
- setLastRefreshed(Date.now());
56
- } finally {
57
- if (fetchGenRef.current === gen) setLoading(false);
56
+ const [savedCurrencies] = usePaneSettingValue<string[]>("currencies", NO_SAVED_CURRENCIES);
57
+ const currencies = useMemo(() => resolveCurrencies(savedCurrencies), [savedCurrencies]);
58
+ const [selectedCurrency, setSelectedCurrency] = useState<string | null>(null);
59
+
60
+ const rates = useFxRatesMap(currencies);
61
+ const status = useMemo(() => readFxStatus(currencies, rates), [currencies, rates]);
62
+
63
+ const refresh = useCallback(() => {
64
+ const coordinator = getSharedMarketDataCoordinator();
65
+ if (!coordinator) return;
66
+ // A rate that is still fresh is left alone by the coordinator; a failed one
67
+ // is retried, which is the case that matters here.
68
+ for (const currency of currencies) {
69
+ if (currency === "USD") continue;
70
+ void coordinator.loadFxRate(currency).catch(() => {});
58
71
  }
59
- }, [dataProvider]);
60
-
61
- useEffect(() => {
62
- fetchRates();
63
- const interval = setInterval(fetchRates, REFRESH_INTERVAL_MS);
64
- return () => clearInterval(interval);
65
- }, [fetchRates]);
66
-
67
- useEffect(() => {
68
- const interval = setInterval(() => setNow(Date.now()), 5_000);
69
- return () => clearInterval(interval);
70
- }, []);
71
-
72
- useShortcut((event) => {
73
- if (!focused) return;
74
- if (event.name === "r") {
75
- fetchRates();
72
+ }, [currencies]);
73
+
74
+ useAutoRefresh(status.latestTs || null, refresh);
75
+
76
+ const columns = useMemo<DataTableColumn[]>(() => [
77
+ { id: "base", label: "", width: BASE_COLUMN_WIDTH, align: "left" },
78
+ ...currencies.map((currency) => ({
79
+ id: currency,
80
+ label: currency,
81
+ width: RATE_COLUMN_WIDTH,
82
+ align: "right" as const,
83
+ })),
84
+ ], [currencies]);
85
+
86
+ const renderCell = useCallback((
87
+ row: MajorCurrency,
88
+ column: DataTableColumn,
89
+ _index: number,
90
+ rowState: { selected: boolean },
91
+ ): DataTableCell => {
92
+ const selectedColor = rowState.selected ? colors.selectedText : undefined;
93
+ if (column.id === "base") {
94
+ return {
95
+ text: row,
96
+ color: selectedColor ?? colors.textBright,
97
+ attributes: TextAttributes.BOLD,
98
+ };
76
99
  }
77
- });
78
100
 
79
- const headerBg = blendHex(colors.bg, colors.border, 0.3);
80
-
81
- function crossRate(row: MajorCurrency, col: MajorCurrency): number | null {
82
- if (row === col) return 1;
83
- const rowUsd = rates.get(row);
84
- const colUsd = rates.get(col);
85
- if (rowUsd == null || colUsd == null) return null;
86
- return rowUsd / colUsd;
87
- }
88
-
89
- const ageText = lastRefreshed ? `updated ${formatAge(now - lastRefreshed)}` : loading ? "loading…" : "";
90
-
91
- usePaneFooter("fx-matrix", () => ({
92
- info: ageText ? [{ id: "updated", parts: [{ text: ageText, tone: loading ? "muted" : "value" }] }] : [],
93
- hints: [{ id: "refresh", key: "r", label: "efresh", onPress: fetchRates }],
94
- }), [ageText, fetchRates, loading]);
95
-
96
- // Row header: just the 3-letter code, no emoji (keeps width predictable)
97
- // Rates use flexGrow so they fill available space dynamically
101
+ const quoteCurrency = column.id as MajorCurrency;
102
+ const dimmed = selectedColor ?? colors.textDim;
103
+ if (row === quoteCurrency) return { text: formatRate(1, quoteCurrency), color: dimmed };
104
+
105
+ const base = rates.get(row);
106
+ const quote = rates.get(quoteCurrency);
107
+ if (base == null || quote == null) {
108
+ // A missing leg must never fall back to parity: 1.0000 on EUR/JPY reads
109
+ // as a real rate.
110
+ const pending = status.loading > 0;
111
+ return { text: pending ? "…" : "—", color: dimmed };
112
+ }
113
+ return { text: formatRate(base / quote, quoteCurrency), color: selectedColor ?? colors.text };
114
+ }, [rates, status.loading]);
115
+
116
+ const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
117
+ if (!isPlainKey(event, "r")) return false;
118
+ event.preventDefault?.();
119
+ refresh();
120
+ return true;
121
+ }, [refresh]);
122
+
123
+ const updatedAgo = useUpdatedAgo(status.latestTs || null);
124
+
125
+ usePaneFooter(FX_MATRIX_PANE_ID, () => {
126
+ const info: PaneFooterSegment[] = [];
127
+ if (status.loading > 0) info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] });
128
+ if (status.unavailable > 0) {
129
+ info.push({
130
+ id: "unavailable",
131
+ parts: [{ text: `${status.unavailable} unavailable`, tone: "warning" }],
132
+ });
133
+ }
134
+ if (updatedAgo) info.push({ id: "updated", parts: [{ text: `updated ${updatedAgo}`, tone: "muted" }] });
135
+ return { info };
136
+ }, [status.loading, status.unavailable, updatedAgo]);
98
137
 
99
138
  return (
100
- <Box flexDirection="column" width={width} height={height}>
101
- {/* Column header row */}
102
- <Box flexDirection="row" paddingX={1} height={1} backgroundColor={headerBg}>
103
- <Box width={5} flexShrink={0} />
104
- {MAJOR_CURRENCIES.map((col) => (
105
- <Box key={col} flexGrow={1} justifyContent="flex-end" paddingRight={1}>
106
- <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>{col}</Text>
107
- </Box>
108
- ))}
109
- </Box>
110
-
111
- {/* Matrix rows */}
112
- <ScrollBox flexGrow={1} scrollY focusable={false}>
113
- <Box flexDirection="column">
114
- {loading && rates.size === 0 ? (
115
- <Box paddingX={1} paddingY={1}>
116
- <Text fg={colors.textMuted}>Fetching rates…</Text>
117
- </Box>
118
- ) : (
119
- MAJOR_CURRENCIES.map((row) => (
120
- <Box key={row} flexDirection="row" paddingX={1}>
121
- <Box width={5} flexShrink={0}>
122
- <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{row}</Text>
123
- </Box>
124
- {MAJOR_CURRENCIES.map((col) => {
125
- const rate = crossRate(row, col);
126
- const isDiag = row === col;
127
- return (
128
- <Box key={col} flexGrow={1} justifyContent="flex-end" paddingRight={1}>
129
- {rate == null ? (
130
- <Text fg={colors.textDim}>—</Text>
131
- ) : (
132
- <Text fg={isDiag ? colors.textDim : colors.text}>
133
- {formatRate(rate, col)}
134
- </Text>
135
- )}
136
- </Box>
137
- );
138
- })}
139
- </Box>
140
- ))
141
- )}
142
- </Box>
143
- </ScrollBox>
144
- </Box>
139
+ <DataTableView<MajorCurrency>
140
+ focused={focused}
141
+ selection={{
142
+ kind: "id",
143
+ selectedId: selectedCurrency ?? currencies[0] ?? null,
144
+ getId: (row) => row,
145
+ onChange: (id) => setSelectedCurrency(id),
146
+ }}
147
+ rootWidth={width}
148
+ rootHeight={height}
149
+ columns={columns}
150
+ items={dataProvider ? currencies : []}
151
+ sortColumnId={null}
152
+ sortDirection="asc"
153
+ onHeaderClick={() => {}}
154
+ getItemKey={(row) => row}
155
+ renderCell={renderCell}
156
+ onRootKeyDown={handleKeyDown}
157
+ emptyStateTitle="No market data provider connected."
158
+ />
145
159
  );
146
160
  }
147
161
 
148
162
  export const fxMatrixModule: PluginModule = {
149
163
  panes: [
150
164
  {
151
- id: "fx-matrix",
165
+ id: FX_MATRIX_PANE_ID,
152
166
  name: "FX Cross Rates",
153
167
  icon: "F",
154
168
  component: FxMatrixPane,
155
169
  defaultPosition: "right",
156
170
  defaultMode: "floating",
157
171
  defaultFloatingSize: { width: 105, height: 14 },
172
+ settings: (context) => ({
173
+ title: "FX Cross Rates Settings",
174
+ values: {
175
+ currencies: resolveCurrencies(context.settings.currencies as string[] | undefined),
176
+ },
177
+ fields: [{
178
+ key: "currencies",
179
+ label: "Currencies",
180
+ type: "ordered-multi-select",
181
+ options: MAJOR_CURRENCIES.map((currency) => ({ value: currency, label: currency })),
182
+ }],
183
+ }),
158
184
  },
159
185
  ],
160
186
 
161
187
  paneTemplates: [
162
188
  {
163
189
  id: "fx-matrix-pane",
164
- paneId: "fx-matrix",
190
+ paneId: FX_MATRIX_PANE_ID,
165
191
  label: "FX Cross Rates",
166
192
  description: "Currency cross-rate matrix for major FX pairs.",
167
193
  keywords: ["fx", "forex", "currency", "exchange", "rates", "cross", "matrix"],
@@ -3,7 +3,18 @@ export const MAJOR_CURRENCIES = ["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD"
3
3
  export type MajorCurrency = typeof MAJOR_CURRENCIES[number];
4
4
 
5
5
  /** Format rate with appropriate precision — JPY pairs get 2 decimals, others get 4 */
6
- export function formatRate(rate: number, toCurrency: MajorCurrency): string {
6
+ export function formatRate(rate: number, toCurrency: string): string {
7
7
  const decimals = toCurrency === "JPY" ? 2 : 4;
8
8
  return rate.toFixed(decimals);
9
9
  }
10
+
11
+ /**
12
+ * The saved currency selection, falling back to the majors when it is empty or
13
+ * holds codes this board does not carry.
14
+ */
15
+ export function resolveCurrencies(saved?: readonly string[]): MajorCurrency[] {
16
+ const resolved = (saved ?? []).filter(
17
+ (code): code is MajorCurrency => MAJOR_CURRENCIES.includes(code as MajorCurrency),
18
+ );
19
+ return resolved.length > 0 ? resolved : [...MAJOR_CURRENCIES];
20
+ }