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
@@ -217,12 +217,6 @@ export const chartComposerModule: PluginModule = {
217
217
  },
218
218
  };
219
219
 
220
- export { CHART_COMPOSER_PANE_ID } from "../../../types/config";
221
- export { ChartComposerPane, ChartComposerResearchTab, ChartComposerSurface } from "./pane";
222
- export { DateWindowDialog } from "./date-window-dialog";
223
- export { SeriesEditorDialog } from "./editor";
224
220
  export * from "./chart-spec";
225
221
  export * from "./presets";
226
222
  export * from "./settings";
227
-
228
- export default chartComposerModule;
@@ -41,6 +41,7 @@ import {
41
41
  buildPriceChartPreset,
42
42
  applySeriesStyle,
43
43
  chartSeriesLabel,
44
+ defaultFinancialTimestampMode,
44
45
  getSelectedBuiltinStudies,
45
46
  getSelectedPairStudies,
46
47
  setBuiltinStudies,
@@ -66,7 +67,12 @@ const RESOLUTION_TABS_WIDTH = RESOLUTION_TABS.reduce(
66
67
  (width, tab) => width + [...tab.label].length + 1,
67
68
  0,
68
69
  );
69
- const AUTO_VIEWPORT_DEBOUNCE_MS = 120;
70
+ const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
71
+
72
+ interface RuntimeChartViewport {
73
+ start: Date;
74
+ end: Date;
75
+ }
70
76
 
71
77
  function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number } | undefined {
72
78
  const x = event?.pixelX;
@@ -76,7 +82,7 @@ function footerAnchorPoint(event?: PaneFooterPressEvent): { x: number; y: number
76
82
  : undefined;
77
83
  }
78
84
 
79
- export interface ChartComposerSurfaceProps {
85
+ interface ChartComposerSurfaceProps {
80
86
  spec: ChartSpec;
81
87
  setSpec: (next: ChartSpec) => void;
82
88
  focused: boolean;
@@ -93,7 +99,7 @@ function isPriceStudyTarget(spec: ChartSpec): boolean {
93
99
  ));
94
100
  }
95
101
 
96
- export function ChartComposerSurface({
102
+ function ChartComposerSurface({
97
103
  spec,
98
104
  setSpec,
99
105
  focused,
@@ -110,6 +116,7 @@ export function ChartComposerSurface({
110
116
  range: spec.viewport.range,
111
117
  resolution: spec.viewport.resolution,
112
118
  dateWindow: spec.viewport.dateWindow ?? null,
119
+ maxPoints: spec.viewport.maxPoints ?? null,
113
120
  sources: spec.series.map((entry) => entry.source.kind === "security"
114
121
  ? [
115
122
  entry.id,
@@ -118,20 +125,31 @@ export function ChartComposerSurface({
118
125
  entry.source.instrument.exchange ?? "",
119
126
  entry.source.fieldId,
120
127
  entry.source.period ?? "auto",
128
+ entry.source.timestampMode
129
+ ?? defaultFinancialTimestampMode(entry.source.fieldId)
130
+ ?? "",
121
131
  ]
122
132
  : [entry.id, entry.source.kind, entry.source.seriesId]),
123
- }), [spec.series, spec.viewport.dateWindow, spec.viewport.range, spec.viewport.resolution]);
124
- const [autoViewportState, setAutoViewportState] = useState<{
133
+ }), [spec.series, spec.viewport.dateWindow, spec.viewport.maxPoints, spec.viewport.range, spec.viewport.resolution]);
134
+ const [runtimeViewportState, setRuntimeViewportState] = useState<{
125
135
  key: string;
126
- viewport: { start: Date; end: Date };
136
+ adaptiveViewport: RuntimeChartViewport | null;
137
+ requestViewport: RuntimeChartViewport;
127
138
  } | null>(null);
128
- const autoViewportTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
129
- const autoViewport = autoViewportState?.key === authoredViewportKey
130
- && spec.viewport.resolution === "auto"
131
- ? autoViewportState.viewport
139
+ const runtimeViewportTimerRef = useRef<ReturnType<typeof globalThis.setTimeout> | null>(null);
140
+ const adaptiveViewportRef = useRef<RuntimeChartViewport | null>(null);
141
+ const requestViewportRef = useRef<RuntimeChartViewport | null>(null);
142
+ const activeRuntimeViewport = runtimeViewportState?.key === authoredViewportKey
143
+ ? runtimeViewportState
132
144
  : null;
133
145
  const targetPointCount = Math.max(60, Math.floor(width * 1.25));
134
- const resolution = useResolvedChartSpec(spec, { autoViewport, targetPointCount });
146
+ const resolution = useResolvedChartSpec(spec, {
147
+ autoViewport: spec.viewport.resolution === "auto"
148
+ ? activeRuntimeViewport?.adaptiveViewport
149
+ : null,
150
+ requestViewport: activeRuntimeViewport?.requestViewport,
151
+ targetPointCount,
152
+ });
135
153
  const selectedStudies = getSelectedBuiltinStudies(spec);
136
154
  const selectedPairStudies = getSelectedPairStudies(spec);
137
155
  const inlineStyleTarget = useMemo(() => getChartInlineStyleTarget(spec), [spec]);
@@ -184,42 +202,55 @@ export function ChartComposerSurface({
184
202
  if (!focused) dispatch({ type: "FOCUS_PANE", paneId });
185
203
  }, [dispatch, focused, paneId]);
186
204
  useEffect(() => {
187
- if (autoViewportTimerRef.current !== null) {
188
- clearTimeout(autoViewportTimerRef.current);
189
- autoViewportTimerRef.current = null;
205
+ if (runtimeViewportTimerRef.current !== null) {
206
+ clearTimeout(runtimeViewportTimerRef.current);
207
+ runtimeViewportTimerRef.current = null;
190
208
  }
191
- setAutoViewportState((current) => current?.key === authoredViewportKey ? current : null);
209
+ adaptiveViewportRef.current = null;
210
+ requestViewportRef.current = null;
211
+ setRuntimeViewportState((current) => current?.key === authoredViewportKey ? current : null);
192
212
  return () => {
193
- if (autoViewportTimerRef.current !== null) {
194
- clearTimeout(autoViewportTimerRef.current);
195
- autoViewportTimerRef.current = null;
213
+ if (runtimeViewportTimerRef.current !== null) {
214
+ clearTimeout(runtimeViewportTimerRef.current);
215
+ runtimeViewportTimerRef.current = null;
196
216
  }
197
217
  };
198
218
  }, [authoredViewportKey]);
199
- const handleChartViewportChange = useCallback((next: { start: Date; end: Date } | null) => {
200
- if (autoViewportTimerRef.current !== null) {
201
- clearTimeout(autoViewportTimerRef.current);
202
- autoViewportTimerRef.current = null;
219
+ const handleChartViewportChange = useCallback((
220
+ next: { start: Date; end: Date } | null,
221
+ interaction: "pan" | "reset" | "sync" | "zoom",
222
+ ) => {
223
+ if (interaction === "sync") return;
224
+ if (runtimeViewportTimerRef.current !== null) {
225
+ clearTimeout(runtimeViewportTimerRef.current);
226
+ runtimeViewportTimerRef.current = null;
203
227
  }
204
- if (spec.viewport.resolution !== "auto" || !next) {
205
- setAutoViewportState(null);
228
+ if (!next) {
229
+ adaptiveViewportRef.current = null;
230
+ requestViewportRef.current = null;
231
+ setRuntimeViewportState(null);
206
232
  return;
207
233
  }
208
234
  const start = next.start.getTime();
209
235
  const end = next.end.getTime();
210
236
  if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return;
211
- autoViewportTimerRef.current = globalThis.setTimeout(() => {
212
- autoViewportTimerRef.current = null;
213
- setAutoViewportState((current) => (
214
- current?.key === authoredViewportKey
215
- && current.viewport.start.getTime() === start
216
- && current.viewport.end.getTime() === end
217
- ? current
218
- : {
219
- key: authoredViewportKey,
220
- viewport: { start: new Date(start), end: new Date(end) },
221
- }
222
- ));
237
+ const viewport = { start: new Date(start), end: new Date(end) };
238
+ requestViewportRef.current = viewport;
239
+ if (interaction === "zoom" && spec.viewport.resolution === "auto") {
240
+ adaptiveViewportRef.current = viewport;
241
+ }
242
+ runtimeViewportTimerRef.current = globalThis.setTimeout(() => {
243
+ runtimeViewportTimerRef.current = null;
244
+ const requestViewport = requestViewportRef.current;
245
+ if (!requestViewport) return;
246
+ const adaptiveViewport = spec.viewport.resolution === "auto"
247
+ ? adaptiveViewportRef.current
248
+ : null;
249
+ setRuntimeViewportState({
250
+ key: authoredViewportKey,
251
+ adaptiveViewport,
252
+ requestViewport,
253
+ });
223
254
  }, AUTO_VIEWPORT_DEBOUNCE_MS);
224
255
  }, [authoredViewportKey, spec.viewport.resolution]);
225
256
 
@@ -564,8 +595,10 @@ export function ChartComposerSurface({
564
595
  <CompositeChart
565
596
  series={plottedSeries}
566
597
  legendSeries={resolution.legendSeries}
598
+ timelineSeries={resolution.timelineSeries}
567
599
  panels={spec.panels}
568
600
  viewport={viewport}
601
+ viewportResetKey={authoredViewportKey}
569
602
  width={Math.max(1, width)}
570
603
  height={Math.max(4, height - 1)}
571
604
  focused={focused}
@@ -30,7 +30,7 @@ import {
30
30
  } from "../../../utils/exchanges";
31
31
  import { MAX_CHART_COMPOSER_SERIES } from "./chart-spec";
32
32
 
33
- export const CHART_FIELD_IDS = {
33
+ const CHART_FIELD_IDS = {
34
34
  price: "market.ohlcv",
35
35
  close: "market.close",
36
36
  volume: "market.volume",
@@ -45,8 +45,6 @@ export const CHART_FIELD_IDS = {
45
45
  evEbitda: "valuation.evEbitda",
46
46
  } as const;
47
47
 
48
- export type ChartComposerPreset = "G" | "GP" | "GIP" | "CMP" | "GF" | "GE";
49
-
50
48
  export type ParsedSeriesExpression =
51
49
  | { kind: "security"; symbol: string; exchange?: string; fieldId: string; label?: string }
52
50
  | { kind: "economic"; provider: "fred"; seriesId: string; label?: string };
@@ -647,22 +645,6 @@ export function buildValuationChartPreset(
647
645
  )));
648
646
  }
649
647
 
650
- export function buildPresetChartSpec(
651
- preset: ChartComposerPreset,
652
- symbols: readonly string[],
653
- expression = "",
654
- ): ChartSpec {
655
- const primary = symbols[0] ?? "";
656
- switch (preset) {
657
- case "G": return buildCustomChartPreset(expression, primary || null);
658
- case "GP": return buildPriceChartPreset(primary);
659
- case "GIP": return buildIntradayPriceChartPreset(primary);
660
- case "CMP": return buildComparisonChartPreset(symbols);
661
- case "GF": return buildFundamentalChartPreset(symbols);
662
- case "GE": return buildValuationChartPreset(symbols);
663
- }
664
- }
665
-
666
648
  const STUDY_DEFAULTS = {
667
649
  volume: { kind: "volume", panelId: "volume", parameters: {} },
668
650
  sma20: { kind: "sma", panelId: "main", parameters: { period: 20 } },
@@ -129,7 +129,7 @@ function reconcileBasePanels(
129
129
  ));
130
130
  }
131
131
 
132
- export function replaceChartSeriesFromExpression(
132
+ function replaceChartSeriesFromExpression(
133
133
  spec: ChartSpec,
134
134
  expression: string,
135
135
  ): ChartSpec {
@@ -364,7 +364,7 @@ export function useChatComposerRuntime({
364
364
  const inputPlaceholder = editingMessage
365
365
  ? t("Edit message...")
366
366
  : replyTo
367
- ? tf("Reply to @{username}...", { username: replyTo.user.username })
367
+ ? tf("Reply to @{username}...", { username: replyTo.user.username ?? "unknown" })
368
368
  : t("Type a message...");
369
369
 
370
370
  return {
@@ -88,7 +88,6 @@ export function useChatContentShortcuts({
88
88
  || event.meta
89
89
  || event.super
90
90
  || event.alt
91
- || event.option
92
91
  ) {
93
92
  return;
94
93
  }
@@ -23,13 +23,18 @@ export async function fetchLatestChannelMessages(
23
23
  const hasIncrementalCursor = !!channel.lastCursor;
24
24
  const hadMessages = channel.messages.length > 0;
25
25
  const countIncrementalUnread = hadMessages && hasIncrementalCursor && !legacyTimestampCursor;
26
+ // The first fetch of a session drops the cursor so anything missed while the
27
+ // socket was connected comes back. Merging is keyed by message id, so
28
+ // re-reading known messages changes nothing and counts no unread.
29
+ const backfillGaps = !channel.backfilled;
26
30
 
27
31
  try {
28
32
  const messages = await apiClient.getMessages(channelId, {
29
33
  limit: MESSAGE_PAGE_SIZE,
30
- after: channel.lastCursor ?? undefined,
34
+ after: backfillGaps ? undefined : channel.lastCursor ?? undefined,
31
35
  });
32
- if (!hasIncrementalCursor && messages.length < MESSAGE_PAGE_SIZE) {
36
+ channel.backfilled = true;
37
+ if ((!hasIncrementalCursor || backfillGaps) && messages.length < MESSAGE_PAGE_SIZE) {
33
38
  channel.reachedOldestMessage = true;
34
39
  }
35
40
  if (messages.length > 0) {
@@ -51,6 +56,7 @@ export async function fetchLatestChannelMessages(
51
56
  return;
52
57
  } catch {
53
58
  const messages = await apiClient.getMessages(channelId, { limit: MESSAGE_PAGE_SIZE });
59
+ channel.backfilled = true;
54
60
  if (messages.length < MESSAGE_PAGE_SIZE) {
55
61
  channel.reachedOldestMessage = true;
56
62
  }
@@ -21,6 +21,7 @@ export interface ChatSessionUser {
21
21
  id: string;
22
22
  username: string;
23
23
  emailVerified: boolean;
24
+ plan?: "free" | "pro";
24
25
  }
25
26
 
26
27
  export function readChatSessionState(
@@ -40,6 +41,7 @@ export function normalizeSessionUser(user: PersistedAuthUser | null | undefined)
40
41
  id: user.id,
41
42
  username: user.username ?? user.name ?? "account",
42
43
  emailVerified: user.emailVerified === true,
44
+ plan: user.plan,
43
45
  }
44
46
  : null;
45
47
  }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { normalizeSessionUser } from "./persistence";
3
+ import { sessionUserFromApiSession } from "./session";
4
+
5
+ describe("chat session market entitlement", () => {
6
+ test("preserves plan through cached and refreshed desktop session mappings", () => {
7
+ expect(
8
+ normalizeSessionUser({
9
+ id: "user-1",
10
+ emailVerified: true,
11
+ username: "vince",
12
+ plan: "pro",
13
+ }),
14
+ ).toMatchObject({ id: "user-1", plan: "pro" });
15
+
16
+ expect(
17
+ sessionUserFromApiSession({
18
+ id: "user-1",
19
+ emailVerified: true,
20
+ username: "vince",
21
+ plan: "free",
22
+ }),
23
+ ).toMatchObject({ id: "user-1", plan: "free" });
24
+ });
25
+ });
@@ -8,6 +8,7 @@ export type ChatApiSession = {
8
8
  username?: string | null;
9
9
  name?: string | null;
10
10
  emailVerified?: boolean | null;
11
+ plan?: "free" | "pro";
11
12
  };
12
13
 
13
14
  export function getSessionIdentity(user: ChatSessionUser | null): string {
@@ -19,6 +20,7 @@ export function sessionUserFromApiSession(session: ChatApiSession): ChatSessionU
19
20
  id: session.id,
20
21
  username: session.username ?? session.name ?? "account",
21
22
  emailVerified: !!session.emailVerified,
23
+ plan: session.plan,
22
24
  };
23
25
  }
24
26
 
@@ -73,6 +73,12 @@ export interface ChannelRuntimeState {
73
73
  refreshMessagesPromise: Promise<void> | null;
74
74
  loadOlderMessagesPromise: Promise<void> | null;
75
75
  reachedOldestMessage: boolean;
76
+ /**
77
+ * False until this session has fetched a full latest page. The incremental
78
+ * cursor only ever asks for newer messages, so a message missed live can
79
+ * never be requested again; one uncursored fetch per session repairs it.
80
+ */
81
+ backfilled: boolean;
76
82
  messages: ChatMessage[];
77
83
  pendingMessages: ChatMessage[];
78
84
  draft: string;
@@ -105,6 +111,7 @@ export function createEmptyChannelState(): ChannelRuntimeState {
105
111
  refreshMessagesPromise: null,
106
112
  loadOlderMessagesPromise: null,
107
113
  reachedOldestMessage: false,
114
+ backfilled: false,
108
115
  messages: [],
109
116
  pendingMessages: [],
110
117
  draft: "",
@@ -501,19 +501,76 @@ describe("ChatController", () => {
501
501
  const calls: Array<{ channelId: string; opts?: { after?: string; before?: string; limit?: number } }> = [];
502
502
  apiClient.getMessages = async (channelId, opts) => {
503
503
  calls.push({ channelId, opts });
504
- return opts?.after === "m1" ? [next] : [];
504
+ if (!opts?.after) return [initial, next];
505
+ return opts.after === "m1" ? [next] : [];
505
506
  };
506
507
 
507
508
  await controller.refreshMessages();
508
509
 
509
- expect(calls).toEqual([{
510
- channelId: "everyone",
511
- opts: { limit: 50, after: "m1" },
512
- }]);
510
+ expect(calls[0]?.opts?.after).toBeUndefined();
513
511
  expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
514
512
  expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
515
513
  lastCursor: "m2",
516
514
  });
515
+
516
+ await controller.refreshMessages();
517
+
518
+ expect(calls[1]?.opts?.after).toBe("m2");
519
+ });
520
+
521
+ test("recovers a message the cursor already moved past", async () => {
522
+ const persistence = new MemoryPersistence();
523
+ const controller = new ChatController();
524
+ const asked: ChatMessage = {
525
+ id: "m1",
526
+ channelId: "everyone",
527
+ content: "why is the chart glitching",
528
+ replyToId: null,
529
+ createdAt: "2026-03-28T00:00:00.000Z",
530
+ user: { id: "u1", username: "vince", displayName: "Vince" },
531
+ };
532
+ const missed: ChatMessage = {
533
+ id: "m2",
534
+ channelId: "everyone",
535
+ content: "charts open with GP",
536
+ replyToId: "m1",
537
+ createdAt: "2026-03-28T00:01:00.000Z",
538
+ user: { id: "u2", username: "gloombot", displayName: "Gloombot" },
539
+ };
540
+ const acknowledged: ChatMessage = {
541
+ id: "m3",
542
+ channelId: "everyone",
543
+ content: "got it",
544
+ replyToId: null,
545
+ createdAt: "2026-03-28T00:02:00.000Z",
546
+ user: { id: "u1", username: "vince", displayName: "Vince" },
547
+ };
548
+
549
+ // The cursor already sits on m3, so an incremental fetch can never ask for
550
+ // m2 again even though the server still has it.
551
+ persistence.setState("channel:everyone", {
552
+ draft: "",
553
+ replyToId: null,
554
+ lastCursor: "m3",
555
+ lastViewedMessageId: "m3",
556
+ }, { schemaVersion: 1 });
557
+ persistence.setResource(TRANSCRIPT_KIND, TRANSCRIPT_KEY, {
558
+ messages: [asked, acknowledged],
559
+ }, {
560
+ sourceKey: TRANSCRIPT_SOURCE,
561
+ schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
562
+ cachePolicy: { staleMs: 1_000, expireMs: 2_000 },
563
+ });
564
+
565
+ controller.attachPersistence(persistence);
566
+
567
+ apiClient.getMessages = async (_channelId, opts) => (
568
+ opts?.after ? [] : [asked, missed, acknowledged]
569
+ );
570
+
571
+ await controller.refreshMessages();
572
+
573
+ expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2", "m3"]);
517
574
  });
518
575
 
519
576
  test("hydrates missing transcript cache without marking history unread", async () => {
@@ -603,19 +660,21 @@ describe("ChatController", () => {
603
660
  const calls: Array<{ channelId: string; opts?: { after?: string; before?: string; limit?: number } }> = [];
604
661
  apiClient.getMessages = async (channelId, opts) => {
605
662
  calls.push({ channelId, opts });
606
- return opts?.after === "m1" ? [fresh] : [];
663
+ return opts?.after ? [] : [cached, fresh];
607
664
  };
608
665
 
609
666
  await controller.refreshMessages();
610
667
 
611
- expect(calls).toEqual([{
612
- channelId: "everyone",
613
- opts: { limit: 50, after: "m1" },
614
- }]);
668
+ expect(calls[0]?.opts?.after).toBeUndefined();
615
669
  expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
616
670
  expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
617
671
  lastCursor: "m2",
618
672
  });
673
+
674
+ await controller.refreshMessages();
675
+
676
+ // The stale m99 cursor never reaches the server.
677
+ expect(calls[1]?.opts?.after).toBe("m2");
619
678
  });
620
679
 
621
680
  test("shows a pending message immediately and replaces it when the send succeeds", async () => {
@@ -1398,16 +1457,10 @@ describe("ChatController", () => {
1398
1457
 
1399
1458
  await controller.refreshMessages();
1400
1459
 
1401
- expect(calls).toEqual([
1402
- {
1403
- channelId: "everyone",
1404
- opts: { limit: 50, after: "2026-03-28T00:00:00.000Z" },
1405
- },
1406
- {
1407
- channelId: "everyone",
1408
- opts: { limit: 50 },
1409
- },
1410
- ]);
1460
+ // The session backfill already ignores the cursor, so the legacy timestamp
1461
+ // is never sent and the old wasted round trip is gone.
1462
+ expect(calls).toHaveLength(1);
1463
+ expect(calls[0]?.opts?.after).toBeUndefined();
1411
1464
  expect(controller.getSnapshot().messages.map((entry) => entry.id)).toEqual(["m1", "m2"]);
1412
1465
  expect(persistence.getState<{ lastCursor: string }>("channel:everyone", { schemaVersion: 1 })).toMatchObject({
1413
1466
  lastCursor: "m2",
@@ -2,6 +2,7 @@ import { memo } from "react";
2
2
  import { Box, Span, Text } from "../../../../ui";
3
3
  import { hoverBg } from "../../../../theme/colors";
4
4
  import { t } from "../../../../i18n";
5
+ import { useAppLanguage } from "../../../../i18n/react";
5
6
  import { MESSAGE_ACTION_WIDTH, normalizeInlinePreview } from "../layout";
6
7
  import { ChatActionChip } from "./action-chip";
7
8
  import { ResponsiveTickerBadgeText } from "./inline-tokens";
@@ -30,6 +31,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
30
31
  }: ChatMessageBaseProps & {
31
32
  registerMessageElement: (messageId: string, node: unknown | null) => void;
32
33
  }) {
34
+ useAppLanguage();
33
35
  const state = getChatMessageRenderState({ msg, index, messages, selectedIdx, hoveredIdx, canSend });
34
36
  const canEditMessage = msg.id === latestEditableMessageId;
35
37
  const showInlineReplyAction = !state.grouped && canSend;
@@ -1,40 +1,21 @@
1
- import { usePaneFooter } from "../../../components";
1
+ import { useMemo } from "react";
2
+ import type { PaneFooterSegment } from "../../../components";
2
3
  import { formatTimeAgo } from "../../../utils/format";
3
4
  import {
4
5
  TWITTER_FEED_PANE_ID,
5
6
  type TwitterFeed,
6
7
  } from "./model";
8
+ import { usePaneStatusFooter } from "../shared/pane-footer";
7
9
 
8
10
  export function useTwitterFeedFooter({
9
11
  activeFeed,
10
- addFeed,
11
- blurSearch,
12
- focusSearch,
13
- removeFeed,
14
- searchFocused,
15
12
  }: {
16
13
  activeFeed: TwitterFeed | null;
17
- addFeed: () => void;
18
- blurSearch: () => void;
19
- focusSearch: () => void;
20
- removeFeed: (feedId: string) => void;
21
- searchFocused: boolean;
22
14
  }) {
23
- usePaneFooter(TWITTER_FEED_PANE_ID, () => ({
24
- info: activeFeed
25
- ? [
26
- { id: "mode", parts: [{ text: activeFeed.queryType, tone: "value" }] },
27
- ...(activeFeed.lastSuccessAt ? [{ id: "last", parts: [{ text: `ran ${formatTimeAgo(new Date(activeFeed.lastSuccessAt))}`, tone: "muted" as const }] }] : []),
28
- ]
29
- : [],
30
- hints: searchFocused
31
- ? [
32
- { id: "done", key: "Esc", label: "done", onPress: blurSearch },
33
- ]
34
- : [
35
- { id: "new", key: "n", label: "ew", onPress: () => addFeed() },
36
- { id: "search", key: "/", label: "search", onPress: focusSearch, disabled: !activeFeed },
37
- { id: "delete", key: "d", label: "elete", onPress: activeFeed ? () => removeFeed(activeFeed.id) : undefined, disabled: !activeFeed },
38
- ],
39
- }), [activeFeed, addFeed, blurSearch, focusSearch, removeFeed, searchFocused]);
15
+ const info = useMemo<PaneFooterSegment[]>(() => (
16
+ activeFeed?.lastSuccessAt
17
+ ? [{ id: "last", parts: [{ text: `ran ${formatTimeAgo(new Date(activeFeed.lastSuccessAt))}`, tone: "muted" }] }]
18
+ : []
19
+ ), [activeFeed?.lastSuccessAt]);
20
+ usePaneStatusFooter({ registrationId: TWITTER_FEED_PANE_ID, info });
40
21
  }
@@ -249,11 +249,6 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
249
249
 
250
250
  useTwitterFeedFooter({
251
251
  activeFeed,
252
- addFeed,
253
- blurSearch,
254
- focusSearch,
255
- removeFeed,
256
- searchFocused,
257
252
  });
258
253
 
259
254
  const searchBar = activeFeed ? (
@@ -1,9 +1,8 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
- import { Box, ScrollBox, Text, TextAttributes, useRendererHost } from "../../../ui";
2
+ import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
3
3
  import {
4
4
  DataTableStackView,
5
5
  TickerBadgeList,
6
- usePaneFooter,
7
6
  type DataTableCell,
8
7
  type DataTableKeyEvent,
9
8
  type DataTableRootKeyContext,
@@ -34,6 +33,7 @@ import {
34
33
  type TweetSortColumnId,
35
34
  type TweetSortDirection,
36
35
  } from "./model";
36
+ import { usePaneStatusLinkFooter } from "../shared/pane-footer";
37
37
 
38
38
  function isAuthError(error: string | null): boolean {
39
39
  return !!error && /unauthorized|verification/i.test(error);
@@ -172,7 +172,6 @@ export function TweetSearchTable({
172
172
  emptyStateTitle?: string;
173
173
  emptyStateHint?: string;
174
174
  }) {
175
- const rendererHost = useRendererHost();
176
175
  const { createPaneFromTemplate } = usePluginAppActions();
177
176
  const { data, loading, error, reload } = useTweetSearchData(requestKey, load, onResult, onError, enabled);
178
177
  const [selectedTweetId, setSelectedTweetId] = useState<string | null>(null);
@@ -186,6 +185,16 @@ export function TweetSearchTable({
186
185
  const selectedIndex = rows.findIndex((tweet) => tweet.id === selectedTweetId);
187
186
  const activeIndex = selectedIndex >= 0 ? selectedIndex : rows.length > 0 ? 0 : -1;
188
187
  const selectedTweet = rows[activeIndex] ?? null;
188
+ const openSelectedTweet = usePaneStatusLinkFooter({
189
+ registrationId: footerId,
190
+ focused,
191
+ url: detailOpen ? selectedTweet?.url : null,
192
+ source: detailOpen && selectedTweet
193
+ ? `@${selectedTweet.author.userName || selectedTweet.author.name}`
194
+ : null,
195
+ loading,
196
+ error,
197
+ });
189
198
 
190
199
  useEffect(() => {
191
200
  if (rows.length === 0) {
@@ -198,9 +207,6 @@ export function TweetSearchTable({
198
207
  }
199
208
  }, [rows, selectedIndex, selectedTweetId]);
200
209
 
201
- const openSelectedTweet = useCallback(() => {
202
- if (selectedTweet?.url) void rendererHost.openExternal(selectedTweet.url);
203
- }, [rendererHost, selectedTweet]);
204
210
  const openUsernameFeed = useCallback((username: string) => {
205
211
  const normalizedUsername = normalizeTwitterUsername(username);
206
212
  if (!normalizedUsername) return;
@@ -247,17 +253,6 @@ export function TweetSearchTable({
247
253
  return true;
248
254
  }, [openSelectedTweet]);
249
255
 
250
- usePaneFooter(footerId, () => ({
251
- info: [
252
- ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
253
- ...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
254
- ],
255
- hints: [
256
- { id: "refresh", key: "r", label: "efresh", onPress: reload },
257
- ...(detailOpen ? [{ id: "open", key: "o", label: "pen", onPress: openSelectedTweet, disabled: !selectedTweet?.url }] : []),
258
- ],
259
- }), [detailOpen, error, footerId, loading, openSelectedTweet, reload, selectedTweet?.url]);
260
-
261
256
  const renderCell = useCallback((
262
257
  tweet: CloudTweetPayload,
263
258
  column: TweetColumn,