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,109 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { testRender } from "../../../renderers/opentui/test-utils";
4
+ import {
5
+ MarketDataCoordinator,
6
+ setSharedMarketDataCoordinator,
7
+ } from "../../../market-data/coordinator";
8
+ import type { QueryEntry } from "../../../market-data/result-types";
9
+ import { AppContext, createInitialState, PaneInstanceProvider } from "../../../state/app/context";
10
+ import { createDefaultConfig } from "../../../types/config";
11
+ import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
12
+ import { fxMatrixModule } from "./index";
13
+
14
+ const FxMatrixPane = fxMatrixModule.panes![0]!.component as (props: {
15
+ paneId: string;
16
+ paneType: string;
17
+ focused: boolean;
18
+ width: number;
19
+ height: number;
20
+ }) => React.ReactNode;
21
+
22
+ function readyEntry(rate: number): QueryEntry<number> {
23
+ return {
24
+ phase: "ready",
25
+ data: rate,
26
+ lastGoodData: rate,
27
+ source: "test",
28
+ fetchedAt: 1,
29
+ staleAt: null,
30
+ error: null,
31
+ attempts: [],
32
+ };
33
+ }
34
+
35
+ function errorEntry(): QueryEntry<number> {
36
+ return {
37
+ phase: "error",
38
+ data: null,
39
+ lastGoodData: null,
40
+ source: "test",
41
+ fetchedAt: null,
42
+ staleAt: null,
43
+ error: { reasonCode: "upstream", message: "no rate" },
44
+ attempts: [],
45
+ };
46
+ }
47
+
48
+ /** Every currency resolves except JPY, which has no rate at all. */
49
+ const RATES: Record<string, number> = { EUR: 1.08, GBP: 1.27, CHF: 1.12, CAD: 0.73, AUD: 0.65, NZD: 0.6 };
50
+
51
+ function installCoordinator(): void {
52
+ setSharedMarketDataCoordinator({
53
+ subscribe: () => () => {},
54
+ subscribeKeys: () => () => {},
55
+ getVersion: () => 1,
56
+ getFxEntry: (currency: string) => (
57
+ RATES[currency] != null ? readyEntry(RATES[currency]!) : errorEntry()
58
+ ),
59
+ loadFxRate: async () => {},
60
+ } as unknown as MarketDataCoordinator);
61
+ }
62
+
63
+ function Harness() {
64
+ const state = createInitialState(createDefaultConfig("/tmp/gloomberb-fxc-pane-test"));
65
+ const runtime = { getMarketData: () => ({}) } as unknown as PluginRuntimeAccess;
66
+
67
+ return (
68
+ <AppContext value={{ state, dispatch: () => {} }}>
69
+ <PluginRenderProvider runtime={runtime} pluginId="market-overview">
70
+ <PaneInstanceProvider paneId="fx-matrix">
71
+ <FxMatrixPane paneId="fx-matrix" paneType="fx-matrix" focused width={100} height={14} />
72
+ </PaneInstanceProvider>
73
+ </PluginRenderProvider>
74
+ </AppContext>
75
+ );
76
+ }
77
+
78
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
79
+
80
+ afterEach(async () => {
81
+ if (testSetup) {
82
+ await act(async () => {
83
+ testSetup!.renderer.destroy();
84
+ });
85
+ testSetup = undefined;
86
+ }
87
+ setSharedMarketDataCoordinator(null);
88
+ });
89
+
90
+ describe("FxMatrixPane", () => {
91
+ test("renders a missing rate as unavailable instead of parity", async () => {
92
+ installCoordinator();
93
+ testSetup = await testRender(<Harness />, { width: 100, height: 14 });
94
+ await act(async () => {
95
+ await testSetup!.renderOnce();
96
+ await testSetup!.renderOnce();
97
+ });
98
+
99
+ const frame = testSetup.captureCharFrame();
100
+ // EUR/USD is a real cross built from the two USD legs.
101
+ expect(frame).toContain("1.0800");
102
+
103
+ // Regression: a currency with no rate rendered 1.0000 against everything.
104
+ const jpyRow = frame.split("\n").find((line) => line.trimStart().startsWith("JPY"));
105
+ expect(jpyRow).toBeDefined();
106
+ expect(jpyRow).not.toContain("1.0000");
107
+ expect(jpyRow).toContain("—");
108
+ });
109
+ });
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { Box, Text, TextAttributes } from "../../../ui";
3
- import { colors, hoverBg } from "../../../theme/colors";
3
+ import { colors } from "../../../theme/colors";
4
4
  import { t } from "../../../i18n";
5
5
 
6
6
  export interface HelpShortcutEntry {
@@ -48,35 +48,6 @@ export function HelpSection({ title, children }: { title: string; children: Reac
48
48
  );
49
49
  }
50
50
 
51
- export function ActionButton({
52
- id,
53
- label,
54
- hovered,
55
- onHover,
56
- onPress,
57
- }: {
58
- id: string;
59
- label: string;
60
- hovered: boolean;
61
- onHover: (id: string | null) => void;
62
- onPress: () => void;
63
- }) {
64
- return (
65
- <Box
66
- backgroundColor={hovered ? hoverBg() : colors.panel}
67
- onMouseOver={() => onHover(id)}
68
- onMouseOut={() => onHover(null)}
69
- onMouseDown={(event: any) => {
70
- event.stopPropagation?.();
71
- event.preventDefault?.();
72
- onPress();
73
- }}
74
- >
75
- <Text fg={hovered ? colors.textBright : colors.text}>{` ${t(label)} `}</Text>
76
- </Box>
77
- );
78
- }
79
-
80
51
  export function ShortcutGroup({ title, entries }: { title: string; entries: HelpShortcutEntry[] }) {
81
52
  return (
82
53
  <Box flexDirection="column" marginTop={1}>
@@ -1,7 +1,7 @@
1
1
  import { Box, ScrollBox, Text, useUiHost } from "../../../ui";
2
2
  import { TextAttributes } from "../../../ui";
3
3
  import { useState } from "react";
4
- import { Tabs } from "../../../components";
4
+ import { Button, Tabs } from "../../../components";
5
5
  import { ExternalLinkText } from "../../../components/ui";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
7
  import type { PluginModule } from "../plugin-module";
@@ -11,7 +11,6 @@ import { getSharedRegistry } from "../../registry";
11
11
  import { usePluginAppActions } from "../../runtime";
12
12
  import { detectShortcutPlatform, formatPrimaryShortcut, getShortcutDisplayMode } from "../../../utils/shortcut-labels";
13
13
  import {
14
- ActionButton,
15
14
  HelpSection,
16
15
  ShortcutGroup,
17
16
  ShortcutRow,
@@ -31,13 +30,12 @@ const HELP_TABS = [
31
30
  ] as const;
32
31
 
33
32
  type HelpTabId = typeof HELP_TABS[number]["value"];
34
- const GLOOMBERB_ISSUES_URL = "https://github.com/vincelwt/gloomberb/issues";
33
+ const GLOOMBERB_ISSUES_URL = "https://github.com/gloom-sh/gloomberb/issues";
35
34
 
36
35
  function HelpPane({ focused, width, height }: PaneProps) {
37
36
  const registry = getSharedRegistry();
38
37
  const { openCommandBar, showPane } = usePluginAppActions();
39
38
  const [activeTabId, setActiveTabId] = useState<HelpTabId>("basics");
40
- const [hoveredAction, setHoveredAction] = useState<string | null>(null);
41
39
  const commandShortcuts = resolveCommandShortcuts(registry);
42
40
  const pluginShortcuts = resolvePluginShortcuts(registry);
43
41
  const windowTemplates = resolveWindowTemplates(registry);
@@ -75,13 +73,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
75
73
  return (
76
74
  <>
77
75
  <Box flexDirection="row" gap={1}>
78
- <ActionButton
79
- id="plugins"
80
- label="Manage Plugins"
81
- hovered={hoveredAction === "plugins"}
82
- onHover={setHoveredAction}
83
- onPress={openPluginManager}
84
- />
76
+ <Button label="Manage Plugins" onPress={openPluginManager} />
85
77
  </Box>
86
78
 
87
79
  <HelpSection title="Command Prefixes">
@@ -306,13 +298,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
306
298
  return (
307
299
  <>
308
300
  <Box flexDirection="row" gap={1}>
309
- <ActionButton
310
- id="debug"
311
- label="Open Debug Log"
312
- hovered={hoveredAction === "debug"}
313
- onHover={setHoveredAction}
314
- onPress={openDebugLog}
315
- />
301
+ <Button label="Open Debug Log" onPress={openDebugLog} />
316
302
  <ExternalLinkText
317
303
  url={GLOOMBERB_ISSUES_URL}
318
304
  label={t("GitHub Issues")}
@@ -339,13 +325,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
339
325
  </Box>
340
326
 
341
327
  <Box flexDirection="row" gap={1}>
342
- <ActionButton
343
- id="layout"
344
- label="Layout Actions"
345
- hovered={hoveredAction === "layout"}
346
- onHover={setHoveredAction}
347
- onPress={openLayoutActions}
348
- />
328
+ <Button label="Layout Actions" onPress={openLayoutActions} />
349
329
  </Box>
350
330
 
351
331
  <HelpSection title="Command Bar">
@@ -99,10 +99,12 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
99
99
  setSelectedId(sortedRows[0]?.id ?? null);
100
100
  }, [selectedId, sortedRows]);
101
101
 
102
+ // Keyed on the holder set itself: re-sorting or a ticking market cap must not
103
+ // restart 25 two-request 13F lookups.
102
104
  useEffect(() => {
103
105
  fundMatchAbortRef.current?.abort();
104
106
  setFundMatches(new Map());
105
- if (sortedRows.length === 0) {
107
+ if (rows.length === 0) {
106
108
  setFundMatching(false);
107
109
  return;
108
110
  }
@@ -110,7 +112,7 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
110
112
  const controller = new AbortController();
111
113
  fundMatchAbortRef.current = controller;
112
114
  setFundMatching(true);
113
- void loadHolder13FMatches(sortedRows, controller.signal)
115
+ void loadHolder13FMatches(rows, controller.signal)
114
116
  .then((matches) => {
115
117
  if (fundMatchAbortRef.current !== controller) return;
116
118
  setFundMatches(matches);
@@ -125,7 +127,7 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
125
127
  fundMatchAbortRef.current = null;
126
128
  }
127
129
  };
128
- }, [sortedRows]);
130
+ }, [rows]);
129
131
 
130
132
  const selectedRow = activeIdx >= 0 ? sortedRows[activeIdx] ?? null : null;
131
133
  const selectedFundMatch = selectedRow ? fundMatches.get(selectedRow.id) ?? null : null;
@@ -169,7 +171,7 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
169
171
  toggleView();
170
172
  return true;
171
173
  }
172
- if ((event.name === "f" || event.name === "enter" || event.name === "return") && selectedFundMatch) {
174
+ if ((event.name === "o" || event.name === "enter" || event.name === "return") && selectedFundMatch) {
173
175
  event.preventDefault?.();
174
176
  event.stopPropagation?.();
175
177
  openFundDetail(selectedRow);
@@ -208,7 +210,7 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
208
210
  toggleView();
209
211
  return;
210
212
  }
211
- if (isPlainKey(event, "f", "enter", "return") && selectedFundMatch) {
213
+ if (isPlainKey(event, "o", "enter", "return") && selectedFundMatch) {
212
214
  event.preventDefault?.();
213
215
  event.stopPropagation?.();
214
216
  openFundDetail(selectedRow);
@@ -261,18 +263,20 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
261
263
  ...(fundMatching ? [{ id: "fund-matching", parts: [{ text: "13F matching", tone: "muted" as const }] }] : []),
262
264
  ],
263
265
  hints: [
264
- { id: "refresh", key: "r", label: "efresh", onPress: refresh },
265
266
  { id: "view", key: "s", label: "witch", onPress: toggleView },
266
- ...(selectedFundMatch ? [{ id: "fund", key: "f", label: "und", onPress: () => openFundDetail(selectedRow) }] : []),
267
+ // `f` is the filter key in sibling panes, so opening a fund uses `o`.
268
+ ...(selectedFundMatch ? [{ id: "fund", key: "o", label: "pen 13F", onPress: () => openFundDetail(selectedRow) }] : []),
267
269
  ],
268
270
  };
269
- }, [data?.asOf, fundMatching, loading, openFundDetail, refresh, selectedFundMatch, selectedRow, toggleView]);
271
+ }, [data?.asOf, fundMatching, loading, openFundDetail, selectedFundMatch, selectedRow, toggleView]);
270
272
 
271
- const emptyTitle = !symbol
272
- ? "No ticker selected"
273
+ // Both views share one status; the treemap must not claim "no chartable
274
+ // values" while the request is still in flight or the pane has no ticker.
275
+ const statusTitle = !symbol
276
+ ? "No ticker selected."
273
277
  : loading
274
278
  ? "Loading holders..."
275
- : error ?? "No holders available";
279
+ : error ?? (sortedRows.length === 0 ? "No holders available" : null);
276
280
  const chartHeight = Math.max(1, height - 1 - (nativePaneChrome ? 1 : 0));
277
281
 
278
282
  return (
@@ -308,11 +312,12 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
308
312
  onHeaderClick={handleHeaderClick}
309
313
  getItemKey={(row) => row.id}
310
314
  renderCell={renderCell}
311
- emptyStateTitle={emptyTitle}
315
+ emptyStateTitle={statusTitle ?? "No holders available"}
312
316
  />
313
317
  ) : (
314
318
  <HoldersTreemap
315
319
  rows={sortedRows}
320
+ emptyStateTitle={statusTitle ?? undefined}
316
321
  width={width}
317
322
  height={chartHeight}
318
323
  selectedId={selectedId}
@@ -8,7 +8,7 @@ import {
8
8
  } from "./format";
9
9
  import type { HolderRow } from "./types";
10
10
 
11
- export function HoldersTreemap({ rows, width, height, selectedId, onSelect, onActivate, currency, marketCap }: {
11
+ export function HoldersTreemap({ rows, width, height, selectedId, onSelect, onActivate, currency, marketCap, emptyStateTitle }: {
12
12
  rows: HolderRow[];
13
13
  width: number;
14
14
  height: number;
@@ -17,6 +17,8 @@ export function HoldersTreemap({ rows, width, height, selectedId, onSelect, onAc
17
17
  onActivate?: (row: HolderRow) => void;
18
18
  currency: string;
19
19
  marketCap?: number;
20
+ /** Loading, error, and no-ticker states the pane resolves for both views. */
21
+ emptyStateTitle?: string;
20
22
  }) {
21
23
  const items = useMemo<Array<MetricTreemapItem<HolderRow>>>(() => rows.map((row) => {
22
24
  const ownership = formatHolderOwnershipLine(row, marketCap);
@@ -43,7 +45,7 @@ export function HoldersTreemap({ rows, width, height, selectedId, onSelect, onAc
43
45
  selectedId={selectedId}
44
46
  onSelect={(item) => onSelect(item.data)}
45
47
  onActivate={onActivate ? (item) => onActivate(item.data) : undefined}
46
- emptyStateTitle="No chartable holder values"
48
+ emptyStateTitle={emptyStateTitle ?? "No chartable holder values"}
47
49
  />
48
50
  );
49
51
  }
@@ -1,4 +1,3 @@
1
- import { Text } from "../../../ui";
2
1
  import { useCallback, useMemo, useState } from "react";
3
2
  import type { PluginModule } from "../plugin-module";
4
3
  import type { SecFilingItem } from "../../../types/data-provider";
@@ -8,8 +7,7 @@ import {
8
7
  } from "../../../market-data/hooks";
9
8
  import { instrumentFromTicker } from "../../../market-data/request-types";
10
9
  import { usePaneTicker } from "../../../state/app/context";
11
- import { colors } from "../../../theme/colors";
12
- import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTableItem } from "../../../components";
10
+ import { EmptyState, FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTableItem } from "../../../components";
13
11
  import { usePluginPaneState } from "../../runtime";
14
12
  import { isUsEquityTicker } from "../../../utils/sec";
15
13
  import { formatCompact, formatCurrency } from "../../../utils/format";
@@ -26,6 +24,10 @@ import {
26
24
  import { useSecFilingContentCache } from "../sec/filing-content";
27
25
 
28
26
  const FORM4_LIMIT = 20;
27
+ // SEC returns one recent-submissions payload per company, so scanning deep for
28
+ // Form 4s costs no extra request; filtering the newest 20 filings of any form
29
+ // hides insider activity behind a burst of 8-Ks.
30
+ const SEC_FILING_SCAN_LIMIT = 300;
29
31
  const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
30
32
 
31
33
  interface ParsedFiling {
@@ -34,7 +36,9 @@ interface ParsedFiling {
34
36
  isLoading: boolean;
35
37
  }
36
38
 
37
- function buildSummary(parsed: ParsedFiling[]): string {
39
+ /** Null while filings are still parsing, so the pane never claims "no activity" early. */
40
+ function buildSummary(parsed: ParsedFiling[]): string | null {
41
+ if (parsed.some(({ isLoading }) => isLoading)) return null;
38
42
  const cutoff = new Date(Date.now() - NINETY_DAYS_MS);
39
43
  let buyShares = 0;
40
44
  let sellShares = 0;
@@ -109,11 +113,11 @@ function InsiderView({ width, height, focused }: { width: number; height: number
109
113
  const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
110
114
 
111
115
  const filingsEntry = useSecFilingsQuery(
112
- instrument && eligibleTicker ? { instrument, count: FORM4_LIMIT } : null,
116
+ instrument && eligibleTicker ? { instrument, count: SEC_FILING_SCAN_LIMIT } : null,
113
117
  );
114
118
  const allFilings = useResolvedEntryValue(filingsEntry) ?? [];
115
119
  const form4Filings = useMemo(
116
- () => allFilings.filter((f) => f.form.trim() === "4"),
120
+ () => allFilings.filter((f) => f.form.trim() === "4").slice(0, FORM4_LIMIT),
117
121
  [allFilings],
118
122
  );
119
123
 
@@ -178,7 +182,7 @@ function InsiderView({ width, height, focused }: { width: number; height: number
178
182
  const selectedFilterName = selectedTransaction?.reportedName ?? null;
179
183
  const pendingLabel = pendingCount > 0 ? `loading ${pendingCount}...` : "";
180
184
  const footerInfo = useMemo(() => [
181
- { id: "summary", parts: [{ text: truncateText(summary, Math.max(24, width - 20)), tone: "muted" as const }] },
185
+ ...(summary ? [{ id: "summary", parts: [{ text: truncateText(summary, Math.max(24, width - 20)), tone: "muted" as const }] }] : []),
182
186
  ...(nameFilter ? [{ id: "filter", parts: [{ text: `filter: ${truncateText(nameFilter, 24)}`, tone: "warning" as const }] }] : []),
183
187
  ...(pendingLabel ? [{ id: "pending", parts: [{ text: pendingLabel, tone: "muted" as const }] }] : []),
184
188
  ], [nameFilter, pendingLabel, summary, width]);
@@ -205,10 +209,12 @@ function InsiderView({ width, height, focused }: { width: number; height: number
205
209
  label: "filing",
206
210
  });
207
211
 
208
- if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view insider activity.</Text>;
212
+ if (!ticker) {
213
+ return <EmptyState title="No ticker selected." message="Select a ticker to view insider activity." />;
214
+ }
209
215
  if (!eligibleTicker) return renderFilingNotice("Insider transactions are only shown for US equities.", width);
210
216
  if (loading && allFilings.length === 0) return <Spinner label="Loading insider filings..." />;
211
- if (error) return renderFilingNotice(`Error: ${error}`, width);
217
+ if (error) return <EmptyState title="Insider filings unavailable." message={error} />;
212
218
  if (!loading && form4Filings.length === 0) {
213
219
  return renderFilingNotice(`No Form 4 filings found for ${ticker.metadata.ticker}.`, width);
214
220
  }
@@ -225,7 +231,11 @@ function InsiderView({ width, height, focused }: { width: number; height: number
225
231
  onRootKeyDown={handleRootKeyDown}
226
232
  sourceLabel="Insider"
227
233
  titleLabel="Transaction"
228
- emptyStateTitle={nameFilter ? "No insider transactions for this filter." : "No insider transactions."}
234
+ emptyStateTitle={nameFilter
235
+ ? "No insider transactions for this filter."
236
+ : pendingCount > 0
237
+ ? "Loading Form 4 transactions..."
238
+ : "No insider transactions."}
229
239
  />
230
240
  );
231
241
  }
@@ -0,0 +1,101 @@
1
+ import type { PluginPersistence } from "../../../types/plugin";
2
+ import { fetchIpoCalendar, type IpoCalendarFetchResult } from "./client";
3
+ import type { IPORecord } from "./types";
4
+
5
+ const CACHE_KIND = "ipo-calendar";
6
+ const CACHE_KEY = "board";
7
+ const CACHE_SOURCE = "stockanalysis";
8
+ const CACHE_SCHEMA_VERSION = 1;
9
+ /**
10
+ * The board moves on pricing days, not minutes, so a quarter hour of freshness
11
+ * is enough; the week-long expiry is what keeps an offline start usable.
12
+ */
13
+ const CACHE_POLICY = {
14
+ staleMs: 15 * 60 * 1000,
15
+ expireMs: 7 * 24 * 60 * 60 * 1000,
16
+ } as const;
17
+
18
+ type PersistedIPORecord = Omit<IPORecord, "date"> & { date: string };
19
+
20
+ export interface IpoCalendarResult {
21
+ records: IPORecord[];
22
+ fetchedAt: number;
23
+ /** True when the network failed and cached records were served instead. */
24
+ stale: boolean;
25
+ /** Endpoints that failed while others succeeded. */
26
+ errors: string[];
27
+ }
28
+
29
+ let persistence: PluginPersistence | null = null;
30
+ let activeFetch: Promise<IpoCalendarResult> | null = null;
31
+
32
+ export function attachIpoCalendarPersistence(next: PluginPersistence): void {
33
+ persistence = next;
34
+ }
35
+
36
+ export function resetIpoCalendarPersistence(): void {
37
+ persistence = null;
38
+ activeFetch = null;
39
+ }
40
+
41
+ function readCache(options?: { allowExpired?: boolean }): IpoCalendarResult | null {
42
+ const record = persistence?.getResource<PersistedIPORecord[]>(CACHE_KIND, CACHE_KEY, {
43
+ sourceKey: CACHE_SOURCE,
44
+ schemaVersion: CACHE_SCHEMA_VERSION,
45
+ allowExpired: options?.allowExpired,
46
+ });
47
+ if (!record || !Array.isArray(record.value)) return null;
48
+ const records = record.value
49
+ .map((entry) => ({ ...entry, date: new Date(entry.date) }))
50
+ .filter((entry) => !Number.isNaN(entry.date.getTime()));
51
+ return { records, fetchedAt: record.fetchedAt, stale: !!record.stale, errors: [] };
52
+ }
53
+
54
+ export function getCachedIpoCalendar(): IpoCalendarResult | null {
55
+ return readCache({ allowExpired: true });
56
+ }
57
+
58
+ /**
59
+ * Serves fresh cache without a request, shares one in-flight fetch across
60
+ * panes, and falls back to whatever is cached when Stock Analysis is
61
+ * unreachable. A partial scrape is cached but keeps its errors so the pane can
62
+ * say so rather than presenting half a board as the whole one.
63
+ */
64
+ export async function loadIpoCalendar(
65
+ force = false,
66
+ loader: () => Promise<IpoCalendarFetchResult> = fetchIpoCalendar,
67
+ ): Promise<IpoCalendarResult> {
68
+ const cached = readCache();
69
+ if (!force && cached && !cached.stale) return cached;
70
+ if (activeFetch) return activeFetch;
71
+
72
+ const fallback = cached ?? readCache({ allowExpired: true });
73
+ activeFetch = loader()
74
+ .then(({ records, errors }) => {
75
+ if (records.length === 0) {
76
+ if (fallback) return { ...fallback, stale: true, errors };
77
+ throw new Error(errors.join("; ") || "Stock Analysis returned no IPOs");
78
+ }
79
+ persistence?.setResource(
80
+ CACHE_KIND,
81
+ CACHE_KEY,
82
+ records.map((record) => ({ ...record, date: record.date.toISOString() })),
83
+ { sourceKey: CACHE_SOURCE, schemaVersion: CACHE_SCHEMA_VERSION, cachePolicy: CACHE_POLICY },
84
+ );
85
+ return { records, fetchedAt: Date.now(), stale: false, errors };
86
+ })
87
+ .catch((error: unknown) => {
88
+ if (fallback) {
89
+ return {
90
+ ...fallback,
91
+ stale: true,
92
+ errors: [error instanceof Error ? error.message : String(error)],
93
+ };
94
+ }
95
+ throw error;
96
+ })
97
+ .finally(() => {
98
+ activeFetch = null;
99
+ });
100
+ return activeFetch;
101
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getFlatData } from "./client";
3
+
4
+ describe("getFlatData", () => {
5
+ test("selects the node with IPO records, not the first object-shaped node", () => {
6
+ const payload = {
7
+ nodes: [
8
+ {
9
+ data: [
10
+ { session: "abc", user: null },
11
+ { cookie: "x" },
12
+ ],
13
+ },
14
+ { type: "skip" },
15
+ {
16
+ data: [
17
+ { type: "root" },
18
+ { s: "AAAA", n: "Alpha", ipoDate: "2026-08-13" },
19
+ { s: "BBBB", n: "Beta", ipoDate: "2026-08-20" },
20
+ ],
21
+ },
22
+ ],
23
+ };
24
+
25
+ const flat = getFlatData(payload);
26
+ expect(flat).not.toBeNull();
27
+ expect(flat).toHaveLength(3);
28
+ expect(flat?.[1]).toEqual({ s: "AAAA", n: "Alpha", ipoDate: "2026-08-13" });
29
+ expect(flat?.[2]).toEqual({ s: "BBBB", n: "Beta", ipoDate: "2026-08-20" });
30
+ });
31
+ });