gloomberb 0.11.0 → 0.11.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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -1,11 +1,12 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { PluginModule } from "../plugin-module";
3
3
  import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
4
4
  import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
5
5
  import { instrumentFromTicker } from "../../../market-data/request-types";
6
6
  import { useDebouncedPluginPaneState } from "../../runtime";
7
7
  import { usePaneTicker } from "../../../state/app/context";
8
- import { EmptyState, FeedDataTableStackView, Spinner, type FeedDataTableItem } from "../../../components";
8
+ import type { ScrollBoxRenderable } from "../../../ui";
9
+ import { EmptyState, FeedDataTableStackView, Spinner, useTableLoadMore, type FeedDataTableItem } from "../../../components";
9
10
  import { isUsEquityTicker } from "../../../utils/sec";
10
11
  import { parseForm4Xml, transactionTypeLabel } from "../insider/insider-data";
11
12
  import { formatCompact, formatCurrency } from "../../../utils/format";
@@ -27,7 +28,8 @@ import {
27
28
  } from "./filing-content";
28
29
  import { usePaneStatusLinkFooter } from "../shared/pane-footer";
29
30
 
30
- const SEC_FILING_LIMIT = 50;
31
+ const SEC_FILING_FETCH_LIMIT = 20_000;
32
+ const SEC_FILING_PAGE_SIZE = 50;
31
33
  const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
32
34
 
33
35
  function getDisplayFormLabel(form: string): string {
@@ -250,10 +252,21 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
250
252
  const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
251
253
  const filingsEntry = useSecFilingsQuery(
252
254
  instrument && eligibleTicker
253
- ? { instrument, count: SEC_FILING_LIMIT }
255
+ ? { instrument, count: SEC_FILING_FETCH_LIMIT }
254
256
  : null,
255
257
  );
256
258
  const filings = useResolvedEntryValue(filingsEntry) ?? [];
259
+ const [visibleCount, setVisibleCount] = useState(SEC_FILING_PAGE_SIZE);
260
+ const visibleFilings = useMemo(() => filings.slice(0, visibleCount), [filings, visibleCount]);
261
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
262
+ const loadMore = useTableLoadMore(
263
+ scrollRef,
264
+ visibleCount < filings.length,
265
+ () => setVisibleCount((current) => Math.min(filings.length, current + SEC_FILING_PAGE_SIZE)),
266
+ );
267
+ useEffect(() => {
268
+ setVisibleCount(SEC_FILING_PAGE_SIZE);
269
+ }, [ticker?.metadata.ticker, ticker?.metadata.exchange]);
257
270
  const loading = filingsEntry?.phase === "loading" || (filingsEntry?.phase === "refreshing" && filings.length === 0);
258
271
  const error = filingsEntry?.phase === "error" ? filingsEntry.error?.message ?? "Failed to load SEC filings" : null;
259
272
 
@@ -270,7 +283,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
270
283
 
271
284
  // Only the filing in view needs its content; queueing every ownership form in
272
285
  // the list fires dozens of SEC requests the user never looks at.
273
- const selectedFiling = filings[selectedIdx];
286
+ const selectedFiling = visibleFilings[selectedIdx];
274
287
  const contentTargets = useMemo(() => [
275
288
  ...(openFiling ? [openFiling] : []),
276
289
  ...buildInlineFilingContentTargets(openFiling, openDocuments),
@@ -283,10 +296,10 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
283
296
  const loadingContent = !!openFiling && !contentCache.has(openFiling.accessionNumber);
284
297
 
285
298
  useEffect(() => {
286
- if (filings.length > 0 && selectedIdx >= filings.length) {
287
- setSelectedIdx(Math.max(0, filings.length - 1));
299
+ if (visibleFilings.length > 0 && selectedIdx >= visibleFilings.length) {
300
+ setSelectedIdx(Math.max(0, visibleFilings.length - 1));
288
301
  }
289
- }, [filings.length, selectedIdx, setSelectedIdx]);
302
+ }, [selectedIdx, setSelectedIdx, visibleFilings.length]);
290
303
 
291
304
  usePaneStatusLinkFooter({
292
305
  registrationId: "sec",
@@ -313,7 +326,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
313
326
  height={height}
314
327
  focused={focused}
315
328
  items={toFeedItems(
316
- filings,
329
+ visibleFilings,
317
330
  openFiling?.accessionNumber,
318
331
  contentCache,
319
332
  loadingContent,
@@ -326,6 +339,8 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
326
339
  sourceLabel="Form"
327
340
  titleLabel="Filing"
328
341
  emptyStateTitle="No SEC filings."
342
+ scrollRef={scrollRef}
343
+ onBodyScrollActivity={loadMore}
329
344
  />
330
345
  );
331
346
  }
@@ -342,6 +342,20 @@ export const sectorsModule: PluginModule = {
342
342
  },
343
343
  ],
344
344
 
345
+ setup(ctx) {
346
+ ctx.registerCommand({
347
+ id: "sectors-sp",
348
+ label: "Sector Performance",
349
+ description: "S&P 500 sector and industry performance sorted by daily change.",
350
+ keywords: ["sector", "sectors", "sp", "performance"],
351
+ category: "data",
352
+ shortcut: "SP",
353
+ execute: () => {
354
+ ctx.createPaneFromTemplate("sectors-pane");
355
+ },
356
+ });
357
+ },
358
+
345
359
  paneTemplates: [
346
360
  {
347
361
  id: "sectors-pane",
@@ -8,7 +8,7 @@ import { useRendererHost } from "../../../ui";
8
8
  import type { RendererHost } from "../../../ui";
9
9
  import { getSharedRegistry } from "../../registry";
10
10
  import { requestAccountManagementTab } from "../account-management/navigation";
11
- import { usePlanAccess, type PlanAccess } from "./plan-access";
11
+ import { resolvePlanAccess, usePlanAccess, type PlanAccess } from "./plan-access";
12
12
 
13
13
  export const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
14
14
 
@@ -19,17 +19,20 @@ export const CLOUD_UPGRADE_URL = "https://gloom.sh/cloud?upgrade=pro";
19
19
  let cloudUpgradeOpener: (() => void) | null = null;
20
20
 
21
21
  /**
22
- * Resolves a safe handoff URL for a captured native session. Signed-out users
23
- * still use the public Cloud page, while signed-in users never expose their
24
- * session cookie to the external browser.
22
+ * Signed-out users get the public Cloud page. Signed-in free accounts go
23
+ * straight to Stripe checkout, and accounts that already have Pro (paying or
24
+ * trialing) get the billing portal, so nobody re-buys a subscription they
25
+ * already hold. Both URLs are account-bound, so no session handoff is needed.
25
26
  */
26
27
  export async function resolveCloudUpgradeUrl(): Promise<string> {
27
- if (!apiClient.getSessionToken()) return CLOUD_UPGRADE_URL;
28
- const handoff = await apiClient.createBrowserHandoff();
29
- return handoff.url;
28
+ if (!apiClient.isSignedIn()) return CLOUD_UPGRADE_URL;
29
+ const { url } = resolvePlanAccess(apiClient.getCurrentUser()).hasProAccess
30
+ ? await apiClient.createBillingPortal()
31
+ : await apiClient.createCloudCheckout();
32
+ return url;
30
33
  }
31
34
 
32
- /** Opens Cloud Pro, transferring a native session with the server's one-time handoff. */
35
+ /** Opens checkout or the billing portal in the user's browser. */
33
36
  export async function openCloudUpgrade(rendererHost: Pick<RendererHost, "openExternal">): Promise<void> {
34
37
  await rendererHost.openExternal(await resolveCloudUpgradeUrl());
35
38
  }
@@ -45,9 +48,13 @@ export function openCloudUpgradeUrl(): boolean {
45
48
  export function useCloudUpgradeAction(): () => void {
46
49
  const rendererHost = useRendererHost();
47
50
  const openUpgrade = useCallback(() => {
48
- void openCloudUpgrade(rendererHost).catch(() => {
49
- // Keep the public pricing page reachable if the short-lived handoff cannot be created.
50
- void rendererHost.openExternal(CLOUD_UPGRADE_URL);
51
+ void openCloudUpgrade(rendererHost).catch(async () => {
52
+ // Keep the Cloud page reachable when checkout cannot be created; a native
53
+ // session still rides along on the server's one-time handoff URL.
54
+ const url = await apiClient.createBrowserHandoff()
55
+ .then((handoff) => handoff.url)
56
+ .catch(() => CLOUD_UPGRADE_URL);
57
+ void rendererHost.openExternal(url);
51
58
  });
52
59
  }, [rendererHost]);
53
60
  useEffect(() => {
@@ -0,0 +1,12 @@
1
+ import { expect, test } from "bun:test";
2
+ import { createPublicPaneShare } from "./public-pane";
3
+
4
+ test("default public pane shares restore no user-owned state", () => {
5
+ const adapter = createPublicPaneShare("World Equity Indices");
6
+ expect(adapter.serialize({
7
+ pane: { instanceId: "world", paneId: "world-indices", settings: { symbols: ["private"] } },
8
+ paneState: { selectedIndex: 2 },
9
+ })).toEqual({ title: "World Equity Indices", data: {} });
10
+ expect(adapter.restore({})).toEqual({});
11
+ expect(adapter.restore({ token: "secret" })).toBeNull();
12
+ });
@@ -0,0 +1,9 @@
1
+ import type { PaneTemplatePublicShareDef } from "../../../types/plugin";
2
+
3
+ /** Shares a public pane whose default template needs no user-owned input. */
4
+ export function createPublicPaneShare(defaultTitle: string): PaneTemplatePublicShareDef {
5
+ return {
6
+ serialize: ({ pane }) => ({ title: pane.title?.trim() || defaultTitle, data: {} }),
7
+ restore: (data) => Object.keys(data).length === 0 ? {} : null,
8
+ };
9
+ }
@@ -0,0 +1,23 @@
1
+ import { useCallback } from "react";
2
+ import { useRendererHost } from "../../../ui";
3
+ import { createShare, publicShareUrl } from "../../../shares/api";
4
+ import type { SharePayload } from "../../../shares/payload";
5
+ import { usePluginAppActions } from "../../runtime";
6
+
7
+ export function usePublicShare(): (payload: SharePayload) => Promise<void> {
8
+ const renderer = useRendererHost();
9
+ const { notify } = usePluginAppActions();
10
+
11
+ return useCallback(async (payload: SharePayload) => {
12
+ try {
13
+ const { id } = await createShare(payload);
14
+ await renderer.copyText(publicShareUrl(id));
15
+ notify({ body: "Share link copied to clipboard", type: "success" });
16
+ } catch (error) {
17
+ notify({
18
+ body: error instanceof Error ? error.message : "Could not create share.",
19
+ type: "error",
20
+ });
21
+ }
22
+ }, [notify, renderer]);
23
+ }
@@ -0,0 +1,32 @@
1
+ import { useCallback } from "react";
2
+ import { useAppStateRef, usePaneInstanceId } from "../../../state/app/context";
3
+ import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
4
+ import { resolveTickerActivation } from "../../ticker-navigation";
5
+ import { usePluginAppActions, usePluginTickerActions } from "../../runtime";
6
+
7
+ /**
8
+ * Shared Enter routing for panes that publish a cursor symbol: reuse the Ticker Research pane
9
+ * already bound to this one instead of stacking another pane on top of it.
10
+ */
11
+ export function useTickerSourceActivate(): (
12
+ symbol: string,
13
+ options?: { newPane?: boolean; floating?: boolean },
14
+ ) => void {
15
+ const paneId = usePaneInstanceId();
16
+ const stateRef = useAppStateRef();
17
+ const { pinTicker } = usePluginTickerActions();
18
+ const { focusPane } = usePluginAppActions();
19
+
20
+ return useCallback((symbol: string, options?: { newPane?: boolean; floating?: boolean }) => {
21
+ const activation = resolveTickerActivation(stateRef.current.config.layout, paneId, options);
22
+ if (activation.kind === "focus") {
23
+ focusPane(activation.paneId);
24
+ return;
25
+ }
26
+ pinTicker(symbol, {
27
+ floating: options?.floating ?? true,
28
+ forceNewPane: options?.newPane,
29
+ paneType: TICKER_RESEARCH_PANE_ID,
30
+ });
31
+ }, [focusPane, paneId, pinTicker, stateRef]);
32
+ }
@@ -31,6 +31,33 @@ describe("createTickerSurfacePaneTemplate", () => {
31
31
  }));
32
32
  });
33
33
 
34
+ test("shares only the public ticker binding and restores it through the template", () => {
35
+ const template = createTickerSurfacePaneTemplate({
36
+ id: "financial-analysis-pane",
37
+ paneId: "financial-analysis",
38
+ label: "Financial Analysis",
39
+ description: "Open financial statements.",
40
+ keywords: ["fa"],
41
+ shortcut: "FA",
42
+ publicShare: true,
43
+ });
44
+
45
+ const shared = template.publicShare?.serialize({
46
+ pane: {
47
+ instanceId: "financial-analysis:AAPL",
48
+ paneId: "financial-analysis",
49
+ title: "FA AAPL",
50
+ binding: { kind: "fixed", symbol: "AAPL" },
51
+ settings: { cachedPrivateValue: "not shared" },
52
+ },
53
+ paneState: { selectedRow: 4 },
54
+ });
55
+
56
+ expect(shared).toEqual({ title: "FA AAPL", data: { symbol: "AAPL" } });
57
+ expect(template.publicShare?.restore(shared!.data)).toEqual({ symbol: "AAPL" });
58
+ expect(template.publicShare?.restore({ symbol: "AAPL", token: "secret" })).toBeNull();
59
+ });
60
+
34
61
  test("keeps chart shortcut variants separate with view keys", () => {
35
62
  const price = createTickerSurfacePaneTemplate({
36
63
  id: "graph-price-pane",
@@ -13,6 +13,8 @@ export interface TickerSurfacePaneTemplateOptions {
13
13
  description: string;
14
14
  keywords: string[];
15
15
  shortcut: string;
16
+ /** Opt in only when this template is available in the hosted browser catalog. */
17
+ publicShare?: boolean;
16
18
  titlePrefix?: string;
17
19
  viewKey?: string | ((
18
20
  symbol: string,
@@ -109,5 +111,20 @@ export function createTickerSurfacePaneTemplate(
109
111
  resolveViewKey(templateOptions.viewKey, symbol, context, createOptions),
110
112
  );
111
113
  },
114
+ ...(templateOptions.publicShare ? {
115
+ publicShare: {
116
+ serialize: ({ pane }) => pane.binding?.kind === "fixed" && pane.binding.symbol.trim()
117
+ ? {
118
+ title: pane.title?.trim() || `${titlePrefix} ${pane.binding.symbol}`,
119
+ data: { symbol: pane.binding.symbol.trim().toUpperCase() },
120
+ }
121
+ : null,
122
+ restore: (data) => {
123
+ if (Object.keys(data).length !== 1 || typeof data.symbol !== "string") return null;
124
+ const symbol = data.symbol.trim().toUpperCase();
125
+ return symbol ? { symbol } : null;
126
+ },
127
+ },
128
+ } : {}),
112
129
  };
113
130
  }
@@ -27,13 +27,17 @@ function cachedDataFromResource<T>(entry: PersistedResourceValue<T> | null): Sub
27
27
 
28
28
  export function getCachedSubstackHome(): SubstackHomeData | null {
29
29
  const subscriptionsEntry = readResource<SubstackPublication[]>("subscriptions", "me", false);
30
- const feedEntry = readResource<SubstackArticleSummary[]>("feed", "subscribed", false);
30
+ const feedEntry = readResource<SubstackArticleSummary[] | { items: SubstackArticleSummary[]; nextCursor?: string | null }>("feed", "subscribed", false);
31
31
  if (!subscriptionsEntry || !feedEntry) return null;
32
+ const feed = Array.isArray(feedEntry.value) ? feedEntry.value : feedEntry.value.items;
33
+ const nextCursor = Array.isArray(feedEntry.value) ? null : feedEntry.value.nextCursor ?? null;
32
34
  return {
33
- subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.value, feedEntry.value),
34
- feed: feedEntry.value,
35
+ subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.value, feed),
36
+ feed,
35
37
  fetchedAt: Math.max(subscriptionsEntry.fetchedAt, feedEntry.fetchedAt),
36
38
  stale: Boolean(subscriptionsEntry.stale) || Boolean(feedEntry.stale),
39
+ hasMore: !!nextCursor,
40
+ nextCursor,
37
41
  };
38
42
  }
39
43
 
@@ -33,13 +33,19 @@ export async function loadSubstackHome(force = false): Promise<SubstackHomeData>
33
33
  }),
34
34
  ]);
35
35
  return {
36
- subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.data, feedEntry.data),
37
- feed: feedEntry.data,
36
+ subscriptions: sortSubscriptionsByLatest(subscriptionsEntry.data, feedEntry.data.items),
37
+ feed: feedEntry.data.items,
38
38
  fetchedAt: Math.max(subscriptionsEntry.fetchedAt, feedEntry.fetchedAt),
39
39
  stale: subscriptionsEntry.stale || feedEntry.stale,
40
+ hasMore: !!feedEntry.data.nextCursor,
41
+ nextCursor: feedEntry.data.nextCursor,
40
42
  };
41
43
  }
42
44
 
45
+ export async function loadSubstackHomeMore(cursor: string): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
46
+ return fetchReaderFeedPage(requireAuth(), "subscribed", cursor);
47
+ }
48
+
43
49
  function payloadCursor(payload: unknown): string | null {
44
50
  const record = payload && typeof payload === "object" && !Array.isArray(payload)
45
51
  ? payload as Record<string, unknown>
@@ -48,23 +54,41 @@ function payloadCursor(payload: unknown): string | null {
48
54
  return typeof cursor === "string" && cursor.trim() ? cursor : null;
49
55
  }
50
56
 
51
- async function fetchReaderFeedItems(auth: ReturnType<typeof requireAuth>, tab: string): Promise<SubstackArticleSummary[]> {
57
+ async function fetchReaderFeedPage(
58
+ auth: ReturnType<typeof requireAuth>,
59
+ tab: string,
60
+ cursor: string | null = null,
61
+ ): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
62
+ const url = new URL(`${SUBSTACK_ORIGIN}/api/v1/reader/feed`);
63
+ url.searchParams.set("tab", tab);
64
+ if (cursor) url.searchParams.set("cursor", cursor);
65
+ const payload = await fetchJsonAuthenticated<unknown>(url.toString(), auth);
66
+ const nextCursor = payloadCursor(payload);
67
+ return {
68
+ items: normalizeFeedItems(payload),
69
+ nextCursor: nextCursor && nextCursor !== cursor ? nextCursor : null,
70
+ };
71
+ }
72
+
73
+ async function fetchReaderFeedItems(
74
+ auth: ReturnType<typeof requireAuth>,
75
+ tab: string,
76
+ ): Promise<{ items: SubstackArticleSummary[]; nextCursor: string | null }> {
52
77
  const collected = new Map<string, SubstackArticleSummary>();
53
78
  let cursor: string | null = null;
54
79
  for (let page = 0; page < READER_FEED_MAX_PAGES && collected.size < READER_FEED_TARGET_ITEMS; page += 1) {
55
- const url = new URL(`${SUBSTACK_ORIGIN}/api/v1/reader/feed`);
56
- url.searchParams.set("tab", tab);
57
- if (cursor) url.searchParams.set("cursor", cursor);
58
- const payload = await fetchJsonAuthenticated<unknown>(url.toString(), auth);
59
- for (const article of normalizeFeedItems(payload)) {
80
+ const result = await fetchReaderFeedPage(auth, tab, cursor);
81
+ for (const article of result.items) {
60
82
  collected.set(article.id, article);
61
83
  if (collected.size >= READER_FEED_TARGET_ITEMS) break;
62
84
  }
63
- const nextCursor = payloadCursor(payload);
64
- if (!nextCursor || nextCursor === cursor) break;
65
- cursor = nextCursor;
85
+ if (!result.nextCursor) {
86
+ cursor = null;
87
+ break;
88
+ }
89
+ cursor = result.nextCursor;
66
90
  }
67
- return [...collected.values()];
91
+ return { items: [...collected.values()], nextCursor: cursor };
68
92
  }
69
93
 
70
94
  export async function loadSubstackPublicationFeed(
@@ -16,6 +16,8 @@ export interface SubstackHomeData {
16
16
  feed: SubstackArticleSummary[];
17
17
  fetchedAt: number;
18
18
  stale: boolean;
19
+ hasMore?: boolean;
20
+ nextCursor?: string | null;
19
21
  }
20
22
 
21
23
  export interface SubstackPublicationFeedPage {
@@ -137,7 +137,7 @@ export function activeFeedStateFromSources({
137
137
  fetchedAt: home.fetchedAt,
138
138
  stale: home.stale,
139
139
  loadingMore: false,
140
- hasMore: false,
140
+ hasMore: home.data?.hasMore === true,
141
141
  nextOffset: null,
142
142
  };
143
143
  }
@@ -3,6 +3,7 @@ import { Box, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
3
3
  import {
4
4
  EmptyState,
5
5
  Spinner,
6
+ useTableLoadMore,
6
7
  type DataTableKeyEvent,
7
8
  } from "../../../components";
8
9
  import type { PaneProps } from "../../../types/plugin";
@@ -15,6 +16,7 @@ import {
15
16
  import {
16
17
  loadSubstackArticleDetail,
17
18
  loadSubstackHome,
19
+ loadSubstackHomeMore,
18
20
  loadSubstackPublicationFeed,
19
21
  } from "./api/loaders";
20
22
  import {
@@ -126,6 +128,33 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
126
128
  });
127
129
  }, [auth, handleAuthFailure]);
128
130
 
131
+ const loadHomeMore = useCallback((cursor: string) => {
132
+ if (!auth) return;
133
+ const gen = homeFetchGenRef.current;
134
+ loadSubstackHomeMore(cursor)
135
+ .then((page) => {
136
+ if (homeFetchGenRef.current !== gen) return;
137
+ setHome((current) => {
138
+ if (!current.data) return current;
139
+ const itemsById = new Map(current.data.feed.map((article) => [article.id, article]));
140
+ for (const article of page.items) itemsById.set(article.id, article);
141
+ return {
142
+ ...current,
143
+ data: {
144
+ ...current.data,
145
+ feed: [...itemsById.values()],
146
+ hasMore: !!page.nextCursor,
147
+ nextCursor: page.nextCursor,
148
+ },
149
+ };
150
+ });
151
+ })
152
+ .catch((loadError) => {
153
+ if (homeFetchGenRef.current !== gen) return;
154
+ handleAuthFailure(loadError);
155
+ });
156
+ }, [auth, handleAuthFailure]);
157
+
129
158
  useEffect(() => {
130
159
  if (!auth) return;
131
160
  loadHome(false);
@@ -215,18 +244,32 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
215
244
  home,
216
245
  }), [activePublication, activePublicationTabId, home, publicationFeeds]);
217
246
 
218
- const loadMoreActivePublicationRows = useCallback(() => {
219
- if (!activePublication || !activePublicationTabId || detailOpen) return;
220
- const entry = publicationFeeds[activePublicationTabId];
221
- const page = entry?.data;
222
- if (!page || entry.loading || entry.loadingMore || entry.error || !page.hasMore || page.nextOffset == null) return;
223
- const scrollBox = tableScrollRef.current;
224
- if (!scrollBox?.viewport) return;
225
- const visibleBottom = scrollBox.scrollTop + scrollBox.viewport.height;
226
- const remainingRows = page.items.length - visibleBottom;
227
- if (remainingRows > PUBLICATION_LOAD_MORE_THRESHOLD_ROWS) return;
228
- loadPublication(activePublication, false, page.nextOffset);
229
- }, [activePublication, activePublicationTabId, detailOpen, loadPublication, publicationFeeds]);
247
+ const loadMoreHome = useCallback(() => {
248
+ if (!auth || home.loading || detailOpen || !home.data?.hasMore || !home.data.nextCursor) return;
249
+ loadHomeMore(home.data.nextCursor);
250
+ }, [auth, detailOpen, home.data?.hasMore, home.data?.nextCursor, home.loading, loadHomeMore]);
251
+
252
+ const loadMoreActivePublicationRows = useTableLoadMore(
253
+ tableScrollRef,
254
+ !detailOpen && (
255
+ activePublication
256
+ ? !!(publicationFeeds[activePublicationTabId ?? ""]?.data?.hasMore
257
+ && publicationFeeds[activePublicationTabId ?? ""]?.data?.nextOffset != null
258
+ && !publicationFeeds[activePublicationTabId ?? ""]?.loading
259
+ && !publicationFeeds[activePublicationTabId ?? ""]?.loadingMore
260
+ && !publicationFeeds[activePublicationTabId ?? ""]?.error)
261
+ : !!(home.data?.hasMore && home.data.nextCursor && !home.loading)
262
+ ),
263
+ () => {
264
+ if (activePublication) {
265
+ const page = publicationFeeds[activePublicationTabId ?? ""]?.data;
266
+ if (page?.nextOffset != null) loadPublication(activePublication, false, page.nextOffset);
267
+ return;
268
+ }
269
+ loadMoreHome();
270
+ },
271
+ PUBLICATION_LOAD_MORE_THRESHOLD_ROWS,
272
+ );
230
273
 
231
274
  const sortedRows = useMemo(() => (
232
275
  sortedSubstackArticles(activeFeedState.data ?? [], sort)
@@ -26,7 +26,7 @@ describe("13F API", () => {
26
26
 
27
27
  const funds = await searchThirteenFFunds("transport-smoke", 1);
28
28
 
29
- expect(urls[0]).toContain("https://forms13f.com/api/v1/funds");
29
+ expect(urls[0]).toContain("/cloud/sec/13f/funds");
30
30
  expect(funds).toEqual([{ cik: "0001067983", name: "BERKSHIRE HATHAWAY INC" }]);
31
31
  });
32
32
 
@@ -7,6 +7,7 @@ import type {
7
7
  ThirteenFTopFund,
8
8
  } from "./types";
9
9
  import type { PluginPersistence } from "../../../types/plugin";
10
+ import { apiClient } from "../../../api-client";
10
11
  import { httpFetch } from "../../../utils/http-transport";
11
12
 
12
13
  const FORMS_13F_BASE_URL = "https://forms13f.com/api/v1";
@@ -124,17 +125,22 @@ async function fetchForms13F<T>(
124
125
  if (cached) return cached;
125
126
  }
126
127
 
127
- const url = `${FORMS_13F_BASE_URL}${path}?${searchParams.toString()}`;
128
- const response = await httpFetch(url, {
129
- headers: { Accept: "application/json" },
130
- signal: options.signal,
131
- });
132
- if (!response.ok) {
133
- const stale = options.cache !== false ? readApiCache<T>(key, { allowExpired: true }) : null;
134
- if (stale) return stale;
135
- throw new Error(`Forms13F ${response.status} for ${path}`);
128
+ let value: T;
129
+ try {
130
+ value = await apiClient.getCloudSec13F(path, params) as T;
131
+ } catch {
132
+ const url = `${FORMS_13F_BASE_URL}${path}?${searchParams.toString()}`;
133
+ const response = await httpFetch(url, {
134
+ headers: { Accept: "application/json" },
135
+ signal: options.signal,
136
+ });
137
+ if (!response.ok) {
138
+ const stale = options.cache !== false ? readApiCache<T>(key, { allowExpired: true }) : null;
139
+ if (stale) return stale;
140
+ throw new Error(`Forms13F ${response.status} for ${path}`);
141
+ }
142
+ value = await response.json() as T;
136
143
  }
137
- const value = await response.json() as T;
138
144
  if (value != null && options.cache !== false) {
139
145
  writeApiCache(key, value);
140
146
  }
@@ -277,6 +283,24 @@ export async function listThirteenFForms(
277
283
  return arrayResponse(raw).map(mapForm).filter((form): form is ThirteenFFormSummary => !!form);
278
284
  }
279
285
 
286
+ export async function listThirteenFFormHoldingsPage(
287
+ cik: string,
288
+ accessionNumber: string,
289
+ signal?: AbortSignal,
290
+ options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
291
+ ): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean }> {
292
+ const offset = Math.max(0, options.offset ?? 0);
293
+ const limit = Math.max(1, options.limit ?? FORM_PAGE_LIMIT);
294
+ const raw = await fetchForms13F<unknown>("/form", {
295
+ cik: normalizeCik(cik),
296
+ accession_number: accessionNumber,
297
+ limit,
298
+ offset,
299
+ }, { signal, forceRefresh: options.forceRefresh });
300
+ const rows = arrayResponse(raw).map(mapHolding).filter((holding): holding is ThirteenFHoldingRecord => !!holding);
301
+ return { rows, hasMore: rows.length >= limit && offset + rows.length < MAX_FORM_ROWS };
302
+ }
303
+
280
304
  export async function listThirteenFFormHoldings(
281
305
  cik: string,
282
306
  accessionNumber: string,
@@ -285,15 +309,13 @@ export async function listThirteenFFormHoldings(
285
309
  ): Promise<ThirteenFHoldingRecord[]> {
286
310
  const rows: ThirteenFHoldingRecord[] = [];
287
311
  for (let offset = 0; offset < MAX_FORM_ROWS; offset += FORM_PAGE_LIMIT) {
288
- const raw = await fetchForms13F<unknown>("/form", {
289
- cik: normalizeCik(cik),
290
- accession_number: accessionNumber,
291
- limit: FORM_PAGE_LIMIT,
312
+ const page = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
313
+ ...options,
292
314
  offset,
293
- }, { signal, forceRefresh: options.forceRefresh });
294
- const page = arrayResponse(raw).map(mapHolding).filter((holding): holding is ThirteenFHoldingRecord => !!holding);
295
- rows.push(...page);
296
- if (page.length < FORM_PAGE_LIMIT) break;
315
+ limit: FORM_PAGE_LIMIT,
316
+ });
317
+ rows.push(...page.rows);
318
+ if (!page.hasMore) break;
297
319
  }
298
320
  return rows;
299
321
  }