gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -1,4 +1,5 @@
1
1
  import type { PricePoint } from "../../../types/financials";
2
+ import { getPricePointTimestamp } from "../../../utils/price-history";
2
3
 
3
4
  export interface DatedReturn {
4
5
  dateKey: string;
@@ -10,21 +11,14 @@ export interface CorrelationResult {
10
11
  sampleSize: number;
11
12
  }
12
13
 
13
- function getPointTimestamp(point: PricePoint): number {
14
- const value = point.date as Date | string | number | null | undefined;
15
- if (value instanceof Date) return value.getTime();
16
- if (value == null) return Number.NaN;
17
- return new Date(value).getTime();
18
- }
19
-
20
14
  function toDateKey(point: PricePoint): string | null {
21
- const timestamp = getPointTimestamp(point);
15
+ const timestamp = getPricePointTimestamp(point);
22
16
  if (!Number.isFinite(timestamp)) return null;
23
17
  return new Date(timestamp).toISOString().slice(0, 10);
24
18
  }
25
19
 
26
20
  function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
27
- return getPointTimestamp(left) - getPointTimestamp(right);
21
+ return getPricePointTimestamp(left) - getPricePointTimestamp(right);
28
22
  }
29
23
 
30
24
  export function computeReturns(closes: number[]): number[] {
@@ -40,7 +34,7 @@ export function computeReturns(closes: number[]): number[] {
40
34
 
41
35
  export function computeDatedReturns(points: PricePoint[]): DatedReturn[] {
42
36
  const sorted = [...points]
43
- .filter((point) => Number.isFinite(getPointTimestamp(point)) && Number.isFinite(point.close))
37
+ .filter((point) => Number.isFinite(getPricePointTimestamp(point)) && Number.isFinite(point.close))
44
38
  .sort(comparePricePointsByDate);
45
39
  const byDate = new Map<string, number>();
46
40
 
@@ -1,7 +1,7 @@
1
1
  import { Box } from "../../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { TextAttributes, type ScrollBoxRenderable } from "../../../ui";
4
- import { DataTableStackView, usePaneFooter, type DataTableCell } from "../../../components";
4
+ import { DataTableStackView, type DataTableCell, type PaneFooterSegment } from "../../../components";
5
5
  import type { PaneProps } from "../../../types/plugin";
6
6
  import type { PluginModule } from "../plugin-module";
7
7
  import { colors, blendHex } from "../../../theme/colors";
@@ -30,6 +30,7 @@ import {
30
30
  type ImpactFilter,
31
31
  } from "./calendar-model";
32
32
  import { attachFredSeriesPersistence } from "../../../data/fred-series";
33
+ import { usePaneStatusFooter } from "../shared/pane-footer";
33
34
 
34
35
  function EconCalendarPane({ focused, width, height }: PaneProps) {
35
36
  const [initialCache] = useState(() => getCalendarCache());
@@ -221,24 +222,21 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
221
222
  ].filter(Boolean).join(" · ") || undefined
222
223
  : undefined;
223
224
 
224
- usePaneFooter("econ-calendar", () => ({
225
- info: [
226
- { id: "impact", parts: [{ text: `impact: ${impactFilter}`, tone: impactFilter === "all" ? "muted" : "value" }] },
227
- { id: "country", parts: [{ text: `country: ${countryFilter}`, tone: countryFilter === "all" ? "muted" : "value" }] },
228
- ...(nextEvent && nextCountdown ? [{
229
- id: "next",
230
- parts: [{ text: `Next: ${nextEvent.event.length > 18 ? nextEvent.event.slice(0, 18).trimEnd() : nextEvent.event} ${nextCountdown}`, tone: "muted" as const }],
231
- }] : []),
232
- ...(staleness ? [{ id: "stale", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
233
- ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
234
- ...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
235
- ],
236
- hints: [
237
- { id: "impact", key: "f", label: "impact", onPress: cycleImpactFilter },
238
- { id: "country", key: "c", label: "ountry", onPress: cycleCountryFilter },
239
- { id: "refresh", key: "r", label: "efresh", onPress: () => load(true) },
240
- ],
241
- }), [countryFilter, cycleCountryFilter, cycleImpactFilter, error, impactFilter, load, loading, nextCountdown, nextEvent?.event, staleness]);
225
+ const calendarStatus = useMemo<PaneFooterSegment[]>(() => [
226
+ ...(impactFilter !== "all" ? [{ id: "impact", parts: [{ text: `impact: ${impactFilter}`, tone: "value" as const }] }] : []),
227
+ ...(countryFilter !== "all" ? [{ id: "country", parts: [{ text: `country: ${countryFilter}`, tone: "value" as const }] }] : []),
228
+ ...(nextEvent && nextCountdown ? [{
229
+ id: "next",
230
+ parts: [{ text: `Next: ${nextEvent.event.length > 18 ? nextEvent.event.slice(0, 18).trimEnd() : nextEvent.event} ${nextCountdown}`, tone: "muted" as const }],
231
+ }] : []),
232
+ ...(staleness ? [{ id: "stale", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
233
+ ], [countryFilter, impactFilter, nextCountdown, nextEvent?.event, staleness]);
234
+ usePaneStatusFooter({
235
+ registrationId: "econ-calendar",
236
+ loading,
237
+ error,
238
+ info: calendarStatus,
239
+ });
242
240
 
243
241
  const handleHeaderClick = useCallback(() => {}, []);
244
242
  const openDisplayRow = useCallback((row: DisplayRow) => {
@@ -5,7 +5,6 @@ export type ViewMode = "table" | "chart";
5
5
  export type HolderColumnId = "holder" | "value" | "shares" | "changeShares" | "changePercent" | "percentHeld" | "reportDate";
6
6
  export type HolderColumn = DataTableColumn & { id: HolderColumnId };
7
7
  export type SortDirection = "asc" | "desc";
8
- export type PreventableMouseEvent = { preventDefault(): void };
9
8
 
10
9
  export interface SortPreference {
11
10
  columnId: HolderColumnId;
@@ -1,10 +1,9 @@
1
1
  import { Text } from "../../../ui";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useMemo, useState } from "react";
3
3
  import type { PluginModule } from "../plugin-module";
4
4
  import type { SecFilingItem } from "../../../types/data-provider";
5
5
  import {
6
6
  useResolvedEntryValue,
7
- useSecFilingContent,
8
7
  useSecFilingsQuery,
9
8
  } from "../../../market-data/hooks";
10
9
  import { instrumentFromTicker } from "../../../market-data/request-types";
@@ -24,6 +23,7 @@ import {
24
23
  formatFilingShortDate,
25
24
  renderFilingNotice,
26
25
  } from "../sec/filing-display";
26
+ import { useSecFilingContentCache } from "../sec/filing-content";
27
27
 
28
28
  const FORM4_LIMIT = 20;
29
29
  const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
@@ -125,34 +125,10 @@ function InsiderView({ width, height, focused }: { width: number; height: number
125
125
  ? (filingsEntry.error?.message ?? "Failed to load SEC filings")
126
126
  : null;
127
127
 
128
- // Fetch content for each Form 4 filing sequentially via an index pointer
129
- const [contentMap, setContentMap] = useState<Map<string, string | null>>(new Map());
130
- const [fetchPointer, setFetchPointer] = useState(0);
131
-
132
- // Reset when ticker changes
133
- useEffect(() => {
134
- setContentMap(new Map());
135
- setFetchPointer(0);
136
- }, [ticker?.metadata.ticker]);
137
-
138
- // Pick the next un-fetched filing to load
139
- const nextToFetch = form4Filings.find((f) => !contentMap.has(f.accessionNumber)) ?? null;
140
- const contentEntry = useSecFilingContent(nextToFetch);
141
- const contentValue = useResolvedEntryValue(contentEntry);
142
-
143
- useEffect(() => {
144
- if (!nextToFetch) return;
145
- if (contentEntry?.phase === "error") {
146
- setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, null));
147
- setFetchPointer((p) => p + 1);
148
- return;
149
- }
150
- if (contentValue !== null && contentValue !== undefined) {
151
- setContentMap((prev) => new Map(prev).set(nextToFetch.accessionNumber, contentValue));
152
- setFetchPointer((p) => p + 1);
153
- }
154
- // eslint-disable-next-line react-hooks/exhaustive-deps
155
- }, [contentValue, contentEntry?.phase, nextToFetch?.accessionNumber, fetchPointer]);
128
+ const { contentCache: contentMap, pendingCount } = useSecFilingContentCache({
129
+ scopeKey: `${ticker?.metadata.ticker ?? "none"}:${ticker?.metadata.exchange ?? ""}`,
130
+ targets: form4Filings,
131
+ });
156
132
 
157
133
  const allParsed: ParsedFiling[] = useMemo(() => form4Filings.map((filing) => {
158
134
  const hasContent = contentMap.has(filing.accessionNumber);
@@ -172,7 +148,6 @@ function InsiderView({ width, height, focused }: { width: number; height: number
172
148
  ), [allParsed, nameFilter]);
173
149
  const feedItems = useMemo(() => toFeedItems(parsed), [parsed]);
174
150
  const summary = useMemo(() => buildSummary(allParsed), [allParsed]);
175
- const pendingCount = form4Filings.filter((f) => !contentMap.has(f.accessionNumber)).length;
176
151
  const selectedTransaction = parsed[selectedIdx]?.transaction ?? null;
177
152
  const openFiling = openItemId
178
153
  ? parsed.find(({ filing }) => filing.accessionNumber === openItemId)?.filing ?? null
@@ -107,17 +107,15 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
107
107
  }
108
108
  }, [news.length, selectedIdx, setSelectedIdx]);
109
109
 
110
- const footerInfo = useMemo(() => [
111
- ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
112
- ...(error ? [{ id: "error", parts: [{ text: "error", tone: "warning" as const }] }] : []),
113
- ...(loadingSummary ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }] : []),
114
- ], [error, loading, loadingSummary]);
115
-
116
110
  useNewsArticleFooter({
117
111
  registrationId: "news",
118
112
  focused,
119
113
  article: openArticle,
120
- info: footerInfo,
114
+ loading,
115
+ error,
116
+ info: loadingSummary
117
+ ? [{ id: "summary", parts: [{ text: "summary loading", tone: "muted" as const }] }]
118
+ : undefined,
121
119
  });
122
120
 
123
121
  if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view news.</Text>;
@@ -10,7 +10,7 @@ import { createStatefulTestPluginRuntime } from "../../../../../test-support/plu
10
10
  import { createDefaultConfig } from "../../../../../types/config";
11
11
  import { Box } from "../../../../../ui";
12
12
  import { PluginRenderProvider } from "../../../../runtime";
13
- import { __setDetectedProvidersForTests } from "../../../ai/providers";
13
+ import { setDetectedProviders } from "../../../ai/providers";
14
14
  import { setAiRunHost } from "../../../ai/runner";
15
15
  import { BreakingPane } from "./pane";
16
16
 
@@ -53,18 +53,16 @@ function createReadyNewsService(articles: NewsArticle[]): { service: NewsService
53
53
  updatedAt: Date.now(),
54
54
  sourceIds: ["test"],
55
55
  };
56
- const listeners = new Set<() => void>();
56
+ const listeners = new Set<(state: NewsQueryState) => void>();
57
57
  let queryStateCalls = 0;
58
58
  const service = {
59
- subscribe(listener: () => void) {
59
+ watchQuery(_query: unknown, listener: (state: NewsQueryState) => void) {
60
60
  listeners.add(listener);
61
+ listener(state);
61
62
  return () => {
62
63
  listeners.delete(listener);
63
64
  };
64
65
  },
65
- getVersion() {
66
- return 1;
67
- },
68
66
  getQueryState() {
69
67
  queryStateCalls += 1;
70
68
  return state;
@@ -110,7 +108,7 @@ function createHarness() {
110
108
  afterEach(async () => {
111
109
  setSharedNewsService(null);
112
110
  setAiRunHost(null);
113
- __setDetectedProvidersForTests(null);
111
+ setDetectedProviders(null);
114
112
  if (testSetup) {
115
113
  await act(async () => {
116
114
  testSetup!.renderer.destroy();
@@ -124,7 +122,7 @@ describe("BreakingPane", () => {
124
122
  let runCalls = 0;
125
123
  const newsService = createReadyNewsService([makeArticle()]);
126
124
  setSharedNewsService(newsService.service);
127
- __setDetectedProvidersForTests([
125
+ setDetectedProviders([
128
126
  {
129
127
  id: "anthropic",
130
128
  name: "Claude",
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useMemo } from "react";
2
- import { useExternalLinkFooter, type PaneFooterSegment, type PaneHint } from "../../../../../components";
2
+ import type { PaneFooterSegment } from "../../../../../components";
3
3
  import { useShortcut } from "../../../../../react/input";
4
4
  import { useRendererHost } from "../../../../../ui";
5
5
  import { apiClient } from "../../../../../api-client";
6
-
7
- const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud";
6
+ import { usePaneStatusLinkFooter } from "../../../shared/pane-footer";
7
+ import { CLOUD_UPGRADE_URL } from "../../../shared/cloud-upgrade";
8
8
 
9
9
  interface NewsFooterArticle {
10
10
  source?: string | null;
@@ -16,7 +16,8 @@ interface UseNewsArticleFooterOptions {
16
16
  focused: boolean;
17
17
  article: NewsFooterArticle | null | undefined;
18
18
  info?: PaneFooterSegment[];
19
- hints?: PaneHint[];
19
+ loading?: boolean;
20
+ error?: string | null;
20
21
  }
21
22
 
22
23
  function hasRealtimeNewsAccess(): boolean {
@@ -29,7 +30,8 @@ export function useNewsArticleFooter({
29
30
  focused,
30
31
  article,
31
32
  info,
32
- hints,
33
+ loading = false,
34
+ error,
33
35
  }: UseNewsArticleFooterOptions) {
34
36
  const rendererHost = useRendererHost();
35
37
  const hasRealtimeAccess = hasRealtimeNewsAccess();
@@ -60,18 +62,15 @@ export function useNewsArticleFooter({
60
62
  parts: [{ text: "delayed 12h, upgrade for realtime", tone: "warning" }],
61
63
  }]
62
64
  ), [hasRealtimeAccess, openUpgrade, showAccessFooter]);
63
- const upgradeHints = useMemo<PaneHint[]>(() => (
64
- showAccessFooter && !hasRealtimeAccess ? [{ id: "upgrade", key: "u", label: "pgrade", onPress: openUpgrade }] : []
65
- ), [hasRealtimeAccess, openUpgrade, showAccessFooter]);
66
65
  const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
67
- const footerHints = useMemo(() => [...upgradeHints, ...(hints ?? [])], [hints, upgradeHints]);
68
66
 
69
- useExternalLinkFooter({
67
+ usePaneStatusLinkFooter({
70
68
  registrationId,
71
69
  focused,
72
70
  url: article?.url,
73
71
  source: article?.source,
74
72
  info: footerInfo,
75
- hints: footerHints,
73
+ loading,
74
+ error,
76
75
  });
77
76
  }
@@ -3,6 +3,7 @@ import {
3
3
  MAX_READ_ARTICLE_IDS,
4
4
  markNewsArticleRead,
5
5
  normalizeNewsReadState,
6
+ type NewsReadState,
6
7
  } from "./read-state";
7
8
 
8
9
  describe("news read state", () => {
@@ -27,4 +28,10 @@ describe("news read state", () => {
27
28
  expect(state.articleIds[0]).toBe("id-0");
28
29
  expect(state.articleIds.at(-1)).toBe(`id-${MAX_READ_ARTICLE_IDS - 1}`);
29
30
  });
31
+
32
+ test("repairs persisted state with a missing article list", () => {
33
+ const state = normalizeNewsReadState({} as NewsReadState);
34
+
35
+ expect(state).toEqual({ articleIds: [] });
36
+ });
30
37
  });
@@ -1,10 +1,9 @@
1
- import { useCallback, useMemo } from "react";
2
- import { usePluginState } from "../../../runtime";
3
1
  import {
4
2
  DEFAULT_MAX_READ_IDS,
5
- markReadId,
6
- normalizeReadIds,
7
- sameStringArray,
3
+ markPersistedReadId,
4
+ normalizePersistedReadIdState,
5
+ usePersistedReadIds,
6
+ type PersistedReadIdAdapter,
8
7
  } from "../../shared/read-state";
9
8
 
10
9
  export interface NewsReadState {
@@ -16,38 +15,29 @@ export const MAX_READ_ARTICLE_IDS = DEFAULT_MAX_READ_IDS;
16
15
 
17
16
  const READ_STATE_KEY = "read-articles";
18
17
  const EMPTY_READ_STATE: NewsReadState = { articleIds: [] };
18
+ const NEWS_READ_STATE_ADAPTER: PersistedReadIdAdapter<NewsReadState> = {
19
+ getIds: (state) => state.articleIds,
20
+ withIds: (_state, articleIds) => ({ articleIds }),
21
+ maxIds: MAX_READ_ARTICLE_IDS,
22
+ };
19
23
 
20
24
  export function normalizeNewsReadState(state: NewsReadState): NewsReadState {
21
- return { articleIds: normalizeReadIds(state.articleIds, MAX_READ_ARTICLE_IDS) };
25
+ return normalizePersistedReadIdState(state, NEWS_READ_STATE_ADAPTER);
22
26
  }
23
27
 
24
28
  export function markNewsArticleRead(
25
29
  state: NewsReadState,
26
30
  articleId: string,
27
31
  ): NewsReadState {
28
- const current = normalizeNewsReadState(state).articleIds;
29
- const articleIds = markReadId(current, articleId, MAX_READ_ARTICLE_IDS);
30
-
31
- return sameStringArray(articleIds, state.articleIds) ? state : { articleIds };
32
+ return markPersistedReadId(state, articleId, NEWS_READ_STATE_ADAPTER);
32
33
  }
33
34
 
34
35
  export function useNewsReadState() {
35
- const [readState, setReadState] = usePluginState<NewsReadState>(
36
- READ_STATE_KEY,
37
- EMPTY_READ_STATE,
38
- { schemaVersion: NEWS_READ_STATE_SCHEMA_VERSION },
39
- );
40
- const normalizedReadState = useMemo(
41
- () => normalizeNewsReadState(readState),
42
- [readState],
43
- );
44
- const readArticleIds = useMemo(
45
- () => new Set(normalizedReadState.articleIds),
46
- [normalizedReadState],
47
- );
48
- const markArticleRead = useCallback((articleId: string) => {
49
- setReadState((current) => markNewsArticleRead(current, articleId));
50
- }, [setReadState]);
51
-
52
- return { readArticleIds, markArticleRead };
36
+ const { readIds, markRead } = usePersistedReadIds({
37
+ key: READ_STATE_KEY,
38
+ fallback: EMPTY_READ_STATE,
39
+ schemaVersion: NEWS_READ_STATE_SCHEMA_VERSION,
40
+ adapter: NEWS_READ_STATE_ADAPTER,
41
+ });
42
+ return { readArticleIds: readIds, markArticleRead: markRead };
53
43
  }
@@ -1,4 +1,5 @@
1
1
  import type { MarketNewsItem } from "../../../../../types/news-source";
2
+ import { decodeHtmlEntities } from "../../../../../utils/html-entities";
2
3
  import { hashString } from "../hash";
3
4
 
4
5
  export interface RssFeedConfig {
@@ -10,17 +11,6 @@ export interface RssFeedConfig {
10
11
  enabled: boolean;
11
12
  }
12
13
 
13
- function decodeHtmlEntities(s: string): string {
14
- return s
15
- .replace(/&amp;/g, "&")
16
- .replace(/&lt;/g, "<")
17
- .replace(/&gt;/g, ">")
18
- .replace(/&quot;/g, '"')
19
- .replace(/&#39;/g, "'")
20
- .replace(/&apos;/g, "'")
21
- .replace(/&nbsp;/g, " ");
22
- }
23
-
24
14
  function stripCdata(s: string): string {
25
15
  return s.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, inner) => inner);
26
16
  }
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { OptionsChain } from "../../../types/financials";
3
+ import { buildOptionsAccessFooterSegment, resolveOptionsAccessFooterState } from "./footer";
4
+
5
+ function chain(overrides: Partial<OptionsChain> = {}): OptionsChain {
6
+ return {
7
+ underlyingSymbol: "AAPL",
8
+ expirationDates: [],
9
+ calls: [],
10
+ puts: [],
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ describe("options access footer", () => {
16
+ test("offers free users the exact delayed upgrade state with a clickable segment", () => {
17
+ const state = resolveOptionsAccessFooterState({
18
+ chain: chain({
19
+ dataSource: "delayed",
20
+ delayMinutes: 15,
21
+ realtimeEligible: false,
22
+ }),
23
+ clientPlan: "free",
24
+ hasLiveQuote: false,
25
+ });
26
+ let upgrades = 0;
27
+ const segment = buildOptionsAccessFooterSegment(state, () => {
28
+ upgrades += 1;
29
+ });
30
+
31
+ expect(state).toEqual({
32
+ canUpgrade: true,
33
+ text: "15-minute delayed options, upgrade for real-time",
34
+ tone: "warning",
35
+ });
36
+ segment.onPress?.();
37
+ expect(upgrades).toBe(1);
38
+ });
39
+
40
+ test("claims real-time only for a Pro user with a fresh live stream", () => {
41
+ expect(
42
+ resolveOptionsAccessFooterState({
43
+ chain: chain({
44
+ dataSource: "delayed",
45
+ delayMinutes: 15,
46
+ realtimeEligible: true,
47
+ }),
48
+ clientPlan: "pro",
49
+ hasLiveQuote: false,
50
+ }),
51
+ ).toMatchObject({
52
+ canUpgrade: false,
53
+ text: "options delayed fallback",
54
+ tone: "warning",
55
+ });
56
+
57
+ expect(
58
+ resolveOptionsAccessFooterState({
59
+ chain: chain({
60
+ dataSource: "live",
61
+ delayMinutes: 0,
62
+ realtimeEligible: true,
63
+ }),
64
+ clientPlan: "pro",
65
+ hasLiveQuote: false,
66
+ }),
67
+ ).toMatchObject({
68
+ canUpgrade: false,
69
+ text: "options delayed fallback",
70
+ tone: "warning",
71
+ });
72
+
73
+ expect(
74
+ resolveOptionsAccessFooterState({
75
+ chain: chain({ realtimeEligible: false }),
76
+ clientPlan: "pro",
77
+ hasLiveQuote: true,
78
+ }).text,
79
+ ).toBe("real-time options");
80
+ });
81
+
82
+ test("makes the current plan authoritative over cached chain metadata", () => {
83
+ expect(
84
+ resolveOptionsAccessFooterState({
85
+ chain: chain({
86
+ dataSource: "live",
87
+ delayMinutes: 0,
88
+ realtimeEligible: true,
89
+ }),
90
+ clientPlan: "free",
91
+ hasLiveQuote: true,
92
+ }),
93
+ ).toMatchObject({
94
+ canUpgrade: true,
95
+ text: "15-minute delayed options, upgrade for real-time",
96
+ tone: "warning",
97
+ });
98
+
99
+ expect(
100
+ resolveOptionsAccessFooterState({
101
+ chain: chain({
102
+ dataSource: "delayed",
103
+ delayMinutes: 15,
104
+ realtimeEligible: false,
105
+ }),
106
+ clientPlan: "pro",
107
+ hasLiveQuote: true,
108
+ }),
109
+ ).toMatchObject({
110
+ canUpgrade: false,
111
+ text: "real-time options",
112
+ tone: "positive",
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,110 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import type { PaneFooterSegment } from "../../../components";
3
+ import { apiClient } from "../../../api-client";
4
+ import { t, tf } from "../../../i18n";
5
+ import { useShortcut } from "../../../react/input";
6
+ import type { OptionsChain } from "../../../types/financials";
7
+ import { useRendererHost } from "../../../ui";
8
+ import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
9
+ import { usePaneStatusFooter } from "../shared/pane-footer";
10
+
11
+ export interface OptionsAccessFooterState {
12
+ canUpgrade: boolean;
13
+ text: string;
14
+ tone: "positive" | "warning";
15
+ }
16
+
17
+ export function resolveOptionsAccessFooterState({
18
+ chain,
19
+ clientPlan,
20
+ hasLiveQuote,
21
+ }: {
22
+ chain: OptionsChain | null | undefined;
23
+ clientPlan: "free" | "pro" | null;
24
+ hasLiveQuote: boolean;
25
+ }): OptionsAccessFooterState {
26
+ if (clientPlan !== "pro") {
27
+ const delayMinutes = chain?.delayMinutes && chain.delayMinutes > 0 ? chain.delayMinutes : 15;
28
+ return {
29
+ canUpgrade: true,
30
+ text: tf("{count}-minute delayed options, upgrade for real-time", {
31
+ count: delayMinutes,
32
+ }),
33
+ tone: "warning",
34
+ };
35
+ }
36
+
37
+ if (hasLiveQuote) {
38
+ return {
39
+ canUpgrade: false,
40
+ text: t("real-time options"),
41
+ tone: "positive",
42
+ };
43
+ }
44
+
45
+ return {
46
+ canUpgrade: false,
47
+ text: t("options delayed fallback"),
48
+ tone: "warning",
49
+ };
50
+ }
51
+
52
+ export function buildOptionsAccessFooterSegment(
53
+ state: OptionsAccessFooterState,
54
+ openUpgrade: () => void,
55
+ ): PaneFooterSegment {
56
+ return {
57
+ id: "options-access",
58
+ ...(state.canUpgrade ? { onPress: openUpgrade } : {}),
59
+ parts: [{ text: state.text, tone: state.tone }],
60
+ };
61
+ }
62
+
63
+ export function useOptionsAccessFooter({
64
+ chain,
65
+ error,
66
+ focused,
67
+ hasLiveQuote,
68
+ loading,
69
+ }: {
70
+ chain: OptionsChain | null | undefined;
71
+ error?: string | null;
72
+ focused: boolean;
73
+ hasLiveQuote: boolean;
74
+ loading?: boolean;
75
+ }): OptionsAccessFooterState {
76
+ const rendererHost = useRendererHost();
77
+ const user = apiClient.getCurrentUser();
78
+ const clientPlan = user?.emailVerified === true && user.plan === "pro" ? "pro" : "free";
79
+ const state = resolveOptionsAccessFooterState({
80
+ chain,
81
+ clientPlan,
82
+ hasLiveQuote,
83
+ });
84
+ const openUpgrade = useCallback(() => {
85
+ void rendererHost.openExternal(CLOUD_UPGRADE_URL);
86
+ }, [rendererHost]);
87
+
88
+ useShortcut(
89
+ (event) => {
90
+ const key = (event.name ?? event.key ?? "").toLowerCase();
91
+ if (!focused || !state.canUpgrade || key !== "u") return;
92
+ event.stopPropagation();
93
+ event.preventDefault();
94
+ openUpgrade();
95
+ },
96
+ { scope: "options:upgrade" },
97
+ );
98
+
99
+ const info = useMemo(
100
+ () => [buildOptionsAccessFooterSegment(state, openUpgrade)],
101
+ [openUpgrade, state.canUpgrade, state.text, state.tone],
102
+ );
103
+ usePaneStatusFooter({
104
+ registrationId: "options",
105
+ loading,
106
+ error,
107
+ info,
108
+ });
109
+ return state;
110
+ }