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
@@ -357,7 +357,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
357
357
  const cachedFinancials = useAppSelector((state) => state.financials);
358
358
  const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
359
359
  const [sessionMarker, setSessionMarker] = useState(() => buildAccountSessionMarker());
360
- const [hasSession, setHasSession] = useState(() => !!apiClient.getSessionToken());
360
+ const [hasSession, setHasSession] = useState(() => apiClient.isSignedIn());
361
361
  const [profile, setProfile] = useState<AccountProfile | null>(null);
362
362
  const [pricing, setPricing] = useState<CloudPricing | null>(null);
363
363
  const [draft, setDraft] = useState<AccountDraft>(() => profileToDraft(null));
@@ -552,7 +552,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
552
552
 
553
553
  useEffect(() => {
554
554
  const unsubscribe = chatController.subscribe((snapshot) => {
555
- setHasSession(!!apiClient.getSessionToken() || snapshot.hasSavedSession);
555
+ setHasSession(apiClient.isSignedIn() || snapshot.hasSavedSession);
556
556
  setSessionMarker(buildAccountSessionMarker());
557
557
  });
558
558
  void chatController.refreshSession().catch(() => {});
@@ -560,7 +560,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
560
560
  }, []);
561
561
 
562
562
  const loadProfile = useCallback(async () => {
563
- if (!apiClient.getSessionToken()) {
563
+ if (!apiClient.isSignedIn()) {
564
564
  setProfile(null);
565
565
  setDraft(profileToDraft(null));
566
566
  return;
@@ -598,7 +598,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
598
598
  }, []);
599
599
 
600
600
  useEffect(() => {
601
- if (!hasSession || !apiClient.getSessionToken()) return;
601
+ if (!hasSession || !apiClient.isSignedIn()) return;
602
602
  if (syncStatus.phase !== "synced" || syncStatus.revision == null) return;
603
603
  if (refreshedSyncRevisionRef.current === syncStatus.revision) return;
604
604
  refreshedSyncRevisionRef.current = syncStatus.revision;
@@ -847,7 +847,7 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
847
847
  turnOffEmailAlerts,
848
848
  });
849
849
 
850
- if (!hasSession && !apiClient.getSessionToken()) {
850
+ if (!hasSession && !apiClient.isSignedIn()) {
851
851
  return (
852
852
  <Box padding={1}>
853
853
  <CloudAuthNotice message={t("Log in to manage your Gloom Cloud account.")} />
@@ -6,7 +6,10 @@ import {
6
6
  resolveDefaultAiProviderId,
7
7
  type AiProvider,
8
8
  } from "./providers";
9
- import { AiScreenerPane } from "./screener/pane";
9
+ import {
10
+ AI_SCREENER_PANE_STATE_KEY,
11
+ AiScreenerPane,
12
+ } from "./screener/pane";
10
13
  import { buildAiScreenerPaneSettingsDef, getAiScreenerPaneSettings } from "./settings";
11
14
  import {
12
15
  LOCAL_AGENT_WORKSPACE_SCHEMA_VERSION,
@@ -50,6 +53,7 @@ import {
50
53
  LIVE_STREAMING_QUICK_SETTING,
51
54
  withLiveStreamingSetting,
52
55
  } from "../shared/live-streaming";
56
+ import { getPluginPaneStateValue } from "../../runtime";
53
57
 
54
58
  function settingOrFallback(
55
59
  settings: Record<string, unknown>,
@@ -269,6 +273,9 @@ export const aiPlugin: GloomPlugin = {
269
273
  defaultPosition: "right",
270
274
  defaultMode: "floating",
271
275
  defaultFloatingSize: { width: 96, height: 32 },
276
+ portableShare: {
277
+ private: { title: true, params: true, settings: true, state: true },
278
+ },
272
279
  settings: (context) => {
273
280
  const providers = detectProviders();
274
281
  const workspaceProviders = providers.filter((provider) => (
@@ -335,7 +342,11 @@ export const aiPlugin: GloomPlugin = {
335
342
  defaultPosition: "right",
336
343
  defaultMode: "floating",
337
344
  defaultFloatingSize: { width: 76, height: 24 },
345
+ tickerSource: true,
338
346
  quickSettings: [LIVE_STREAMING_QUICK_SETTING],
347
+ portableShare: {
348
+ private: { title: true, params: true, settings: true, state: true },
349
+ },
339
350
  settings: (context) => {
340
351
  const providers = detectProviders();
341
352
  const screenerProviders = providers.filter((provider) => (
@@ -347,7 +358,12 @@ export const aiPlugin: GloomPlugin = {
347
358
  const currentFallbackProviderId = resolveDefaultAiProviderId(screenerRunners);
348
359
  const defaults = defaultsFromConfig(context.config, currentFallbackProviderId);
349
360
  const providerIds = new Set(screenerProviders.map((provider) => provider.id));
350
- const persisted = ctx.resume.getState<PersistedAiScreenerPaneState>(
361
+ const persisted = getPluginPaneStateValue<PersistedAiScreenerPaneState | null>(
362
+ context.paneState,
363
+ "ai",
364
+ AI_SCREENER_PANE_STATE_KEY,
365
+ null,
366
+ ) ?? ctx.resume.getState<PersistedAiScreenerPaneState>(
351
367
  `screener-pane:${context.paneId}`,
352
368
  { schemaVersion: 1 },
353
369
  ) ?? EMPTY_PANE_STATE;
@@ -1,8 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { act, useReducer, useState } from "react";
2
+ import { act, useEffect, useReducer, useState } from "react";
3
3
  import { TestDialogProvider, testRender } from "../../../../renderers/opentui/test-utils";
4
4
  import { PaneFooterBar, PaneFooterProvider } from "../../../../components/layout/pane/footer";
5
- import { AppContext, PaneInstanceProvider, appReducer, createInitialState } from "../../../../state/app/context";
5
+ import {
6
+ AppContext,
7
+ PaneInstanceProvider,
8
+ appReducer,
9
+ createInitialState,
10
+ type PaneRuntimeState,
11
+ } from "../../../../state/app/context";
6
12
  import { createDefaultConfig } from "../../../../types/config";
7
13
  import type { Quote, TickerFinancials } from "../../../../types/financials";
8
14
  import type { TickerRecord } from "../../../../types/ticker";
@@ -11,7 +17,8 @@ import { createStatefulTestPluginRuntime } from "../../../../test-support/plugin
11
17
  import { Box } from "../../../../ui";
12
18
  import { PluginRenderProvider, type PluginRuntimeAccess } from "../../../runtime";
13
19
  import { getSharedMarketData, setSharedMarketDataForTests, setSharedRegistryForTests } from "../../../registry";
14
- import { AiScreenerPane } from "./pane";
20
+ import { AI_SCREENER_PANE_STATE_KEY, AiScreenerPane } from "./pane";
21
+ import { createScreenerTab, EMPTY_PANE_STATE } from "./model";
15
22
  import { setDetectedProviders, type AiProvider } from "../providers";
16
23
  import { setAiRunHost, setAiRuntimeCatalog, type AiRunHost } from "../runner";
17
24
 
@@ -99,10 +106,14 @@ function ScreenerHarness({
99
106
  prompt,
100
107
  providerId,
101
108
  settings,
109
+ runtime: runtimeOverride,
110
+ onPaneState,
102
111
  }: {
103
112
  prompt: string;
104
113
  providerId: string;
105
114
  settings?: Record<string, unknown>;
115
+ runtime?: PluginRuntimeAccess;
116
+ onPaneState?: (state: PaneRuntimeState | undefined) => void;
106
117
  }) {
107
118
  const config = createDefaultConfig("/tmp/gloomberb-ai-screener");
108
119
  config.layout.instances.push({
@@ -129,7 +140,11 @@ function ScreenerHarness({
129
140
  ]);
130
141
  return initial;
131
142
  });
132
- const [runtime] = useState(() => makeRuntime());
143
+ const [runtime] = useState(() => runtimeOverride ?? makeRuntime());
144
+
145
+ useEffect(() => {
146
+ onPaneState?.(state.paneState[PANE_ID]);
147
+ }, [onPaneState, state.paneState]);
133
148
 
134
149
  return (
135
150
  <AppContext value={{ state, dispatch }}>
@@ -185,6 +200,34 @@ afterEach(() => {
185
200
  });
186
201
 
187
202
  describe("AiScreenerPane", () => {
203
+ test("migrates legacy global tabs into pane-local state", async () => {
204
+ const runtime = makeRuntime();
205
+ const legacyState = {
206
+ tabs: [createScreenerTab("Find legacy candidates.", "anthropic")],
207
+ };
208
+ runtime.setResumeState("ai", `screener-pane:${PANE_ID}`, legacyState, 1);
209
+ setDetectedProviders([makeProvider("anthropic", "Claude")]);
210
+ let paneState: PaneRuntimeState | undefined;
211
+
212
+ testSetup = await testRender(
213
+ <ScreenerHarness
214
+ prompt=""
215
+ providerId="anthropic"
216
+ runtime={runtime}
217
+ onPaneState={(state) => { paneState = state; }}
218
+ />,
219
+ { width: 96, height: 18 },
220
+ );
221
+
222
+ await act(async () => {
223
+ await testSetup!.renderOnce();
224
+ await Bun.sleep(0);
225
+ await testSetup!.renderOnce();
226
+ });
227
+ expect(paneState?.pluginState?.ai?.[AI_SCREENER_PANE_STATE_KEY]).toEqual(legacyState);
228
+ expect(runtime.getResumeState("ai", `screener-pane:${PANE_ID}`, 1)).toEqual(EMPTY_PANE_STATE);
229
+ });
230
+
188
231
  test("shows the account connection message before trying to run", async () => {
189
232
  const provider = makeProvider("anthropic", "Claude");
190
233
  setDetectedProviders([provider]);
@@ -1,20 +1,20 @@
1
1
  import { Box, Text } from "../../../../ui";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from "react";
3
3
  import type { PaneProps } from "../../../../types/plugin";
4
- import { TICKER_RESEARCH_PANE_ID } from "../../../../types/config";
5
4
  import {
6
5
  useAppDispatch,
7
6
  useAppSelector,
8
7
  usePaneInstance,
9
8
  usePaneInstanceId,
9
+ usePaneStateValue,
10
10
  } from "../../../../state/app/context";
11
11
  import {
12
12
  useAssetData,
13
13
  usePluginConfigState,
14
14
  usePluginPaneState,
15
15
  usePluginState,
16
- usePluginTickerActions,
17
16
  } from "../../../runtime";
17
+ import { useTickerSourceActivate } from "../../shared/ticker-source";
18
18
  import { type DataTableKeyEvent } from "../../../../components";
19
19
  import { colors } from "../../../../theme/colors";
20
20
  import { t } from "../../../../i18n";
@@ -55,9 +55,11 @@ import {
55
55
  import { AGE_TICK_MS } from "../../shared/auto-refresh";
56
56
  import { useLiveStreamingSetting } from "../../shared/live-streaming";
57
57
 
58
+ export const AI_SCREENER_PANE_STATE_KEY = "screener";
59
+
58
60
  export function AiScreenerPane({ focused, width, height }: PaneProps) {
59
61
  const dataProvider = useAssetData();
60
- const { pinTicker } = usePluginTickerActions();
62
+ const activateTicker = useTickerSourceActivate();
61
63
  const paneId = usePaneInstanceId();
62
64
  const paneInstance = usePaneInstance();
63
65
  const liveStreaming = useLiveStreamingSetting();
@@ -79,13 +81,27 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
79
81
  );
80
82
  const defaultProviderId = defaults.providerId;
81
83
  const defaultModelId = defaults.modelId;
82
- const [persistedState, setPersistedState] = usePluginState<PersistedAiScreenerPaneState>(
84
+ const [legacyPersistedState, setLegacyPersistedState] = usePluginState<PersistedAiScreenerPaneState>(
83
85
  `screener-pane:${paneId}`,
84
86
  EMPTY_PANE_STATE,
85
87
  { schemaVersion: 1 },
86
88
  );
89
+ const [panePersistedState, setPanePersistedState] = usePluginPaneState<PersistedAiScreenerPaneState | null>(
90
+ AI_SCREENER_PANE_STATE_KEY,
91
+ null,
92
+ );
93
+ const persistedState = panePersistedState ?? legacyPersistedState;
94
+ const setPersistedState = useCallback((nextValue: SetStateAction<PersistedAiScreenerPaneState>) => {
95
+ setPanePersistedState((currentValue) => {
96
+ const previousValue = currentValue ?? legacyPersistedState;
97
+ return typeof nextValue === "function"
98
+ ? (nextValue as (previous: PersistedAiScreenerPaneState) => PersistedAiScreenerPaneState)(previousValue)
99
+ : nextValue;
100
+ });
101
+ }, [legacyPersistedState, setPanePersistedState]);
87
102
  const [activeTabId, setActiveTabId] = usePluginPaneState<string | null>("activeTabId", null);
88
- const [cursorSymbol, setCursorSymbol] = usePluginPaneState<string | null>("cursorSymbol", null);
103
+ // Top-level pane state, not plugin-scoped: this is what follower panes resolve.
104
+ const [cursorSymbol, setCursorSymbol] = usePaneStateValue<string | null>("cursorSymbol", null);
89
105
  const [sorts, setSorts] = usePluginPaneState<Record<string, ScreenerSortPreference>>("sorts", {});
90
106
  const [now, setNow] = useState(Date.now());
91
107
  const initializedRef = useRef(false);
@@ -98,6 +114,15 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
98
114
  }, []);
99
115
 
100
116
  const tabs = useMemo(() => normalizeTabs(persistedState), [persistedState]);
117
+
118
+ // ponytail: legacy global data is ambiguous across layouts; the first mounted pane claims it.
119
+ useEffect(() => {
120
+ const legacyTabs = normalizeTabs(legacyPersistedState);
121
+ if (legacyTabs.length === 0) return;
122
+ if (panePersistedState === null) setPanePersistedState({ tabs: legacyTabs });
123
+ setLegacyPersistedState(EMPTY_PANE_STATE);
124
+ }, [legacyPersistedState, panePersistedState, setLegacyPersistedState, setPanePersistedState]);
125
+
101
126
  const paneSettings = useMemo(
102
127
  () => getAiScreenerPaneSettings(paneInstance?.settings),
103
128
  [paneInstance?.settings],
@@ -301,12 +326,9 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
301
326
  if (!isEnter || !cursorSymbol) return false;
302
327
  event.preventDefault?.();
303
328
  event.stopPropagation?.();
304
- pinTicker(cursorSymbol, {
305
- floating: !!event.shift,
306
- paneType: TICKER_RESEARCH_PANE_ID,
307
- });
329
+ activateTicker(cursorSymbol, { floating: !!event.shift, newPane: !!event.shift });
308
330
  return true;
309
- }, [cursorSymbol, pinTicker]);
331
+ }, [activateTicker, cursorSymbol]);
310
332
 
311
333
  const contentHeight = Math.max(height - 3, 4);
312
334
  const editorProvider = editorState ? getAiProvider(editorState.providerId, providers) : null;
@@ -406,7 +428,8 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
406
428
  onHeaderClick={handleHeaderClick}
407
429
  onRootKeyDown={handleTableKeyDown}
408
430
  onRowActivate={(ticker) => {
409
- pinTicker(ticker.metadata.ticker, { floating: true, paneType: TICKER_RESEARCH_PANE_ID });
431
+ setCursorSymbol(ticker.metadata.ticker);
432
+ activateTicker(ticker.metadata.ticker, { floating: true });
410
433
  }}
411
434
  />
412
435
  )}
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { evaluateAlert, createAlert, serializeAlerts, deserializeAlerts } from "./alert-engine";
2
+ import { evaluateAlert, createAlert, editAlert, rearmAlert, serializeAlerts, deserializeAlerts } from "./alert-engine";
3
3
 
4
4
  describe("evaluateAlert", () => {
5
5
  test("above: triggers when price exceeds target", () => {
@@ -41,6 +41,58 @@ describe("evaluateAlert", () => {
41
41
  });
42
42
  });
43
43
 
44
+ describe("editAlert", () => {
45
+ test("keeps identity, re-arms, and clears trigger/quote lifecycle state", () => {
46
+ const alert = {
47
+ ...createAlert("aapl", "above", 200, "NASDAQ"),
48
+ message: "watch this",
49
+ status: "triggered" as const,
50
+ triggeredAt: 123,
51
+ lastCheckedPrice: 205,
52
+ lastCheckedAt: 124,
53
+ lastCheckError: "boom",
54
+ lastQuoteUpdatedAt: 125,
55
+ lastQuoteSource: "live" as const,
56
+ lastQuoteProviderId: "yahoo",
57
+ };
58
+
59
+ const edited = editAlert(alert, "aapl", "crosses", 180);
60
+
61
+ expect(edited).toEqual({
62
+ id: alert.id,
63
+ symbol: "AAPL",
64
+ exchange: "NASDAQ",
65
+ condition: "crosses",
66
+ targetPrice: 180,
67
+ createdAt: alert.createdAt,
68
+ status: "active",
69
+ message: "watch this",
70
+ });
71
+ // A stale lastCheckedPrice would make `crosses` fire off the previous symbol's baseline.
72
+ expect(evaluateAlert(edited, 185)).toBe(false);
73
+ });
74
+
75
+ test("rearming a crosses alert drops the old quote baseline", () => {
76
+ const alert = {
77
+ ...createAlert("AAPL", "crosses", 180),
78
+ status: "triggered" as const,
79
+ triggeredAt: 123,
80
+ lastCheckedPrice: 185,
81
+ };
82
+
83
+ const rearmed = rearmAlert(alert);
84
+
85
+ expect(rearmed.status).toBe("active");
86
+ expect(rearmed.lastCheckedPrice).toBeUndefined();
87
+ expect(evaluateAlert(rearmed, 175)).toBe(false);
88
+ });
89
+
90
+ test("drops the exchange when the symbol changes", () => {
91
+ const alert = createAlert("AAPL", "above", 200, "NASDAQ");
92
+ expect(editAlert(alert, "tsla", "above", 200).exchange).toBeUndefined();
93
+ });
94
+ });
95
+
44
96
  describe("serializeAlerts / deserializeAlerts", () => {
45
97
  test("roundtrips alerts", () => {
46
98
  const alerts = [createAlert("AAPL", "above", 200), createAlert("TSLA", "below", 100)];
@@ -2,10 +2,16 @@ import type { AlertCondition, AlertRule } from "./types";
2
2
 
3
3
  export type { AlertRule };
4
4
 
5
- export function createAlert(symbol: string, condition: AlertCondition, targetPrice: number): AlertRule {
5
+ export function createAlert(
6
+ symbol: string,
7
+ condition: AlertCondition,
8
+ targetPrice: number,
9
+ exchange?: string,
10
+ ): AlertRule {
6
11
  return {
7
12
  id: `alert-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
8
13
  symbol: symbol.toUpperCase(),
14
+ exchange: exchange?.trim() || undefined,
9
15
  condition,
10
16
  targetPrice,
11
17
  createdAt: Date.now(),
@@ -13,6 +19,33 @@ export function createAlert(symbol: string, condition: AlertCondition, targetPri
13
19
  };
14
20
  }
15
21
 
22
+ /**
23
+ * Rebuilt from a whitelist rather than spread so every trigger/quote lifecycle
24
+ * field is dropped: a re-armed `crosses` alert must start from a fresh baseline.
25
+ */
26
+ export function editAlert(
27
+ alert: AlertRule,
28
+ symbol: string,
29
+ condition: AlertCondition,
30
+ targetPrice: number,
31
+ ): AlertRule {
32
+ const nextSymbol = symbol.trim().toUpperCase();
33
+ return {
34
+ id: alert.id,
35
+ symbol: nextSymbol,
36
+ exchange: nextSymbol === alert.symbol.trim().toUpperCase() ? alert.exchange : undefined,
37
+ condition,
38
+ targetPrice,
39
+ createdAt: alert.createdAt,
40
+ status: "active",
41
+ message: alert.message,
42
+ };
43
+ }
44
+
45
+ export function rearmAlert(alert: AlertRule): AlertRule {
46
+ return editAlert(alert, alert.symbol, alert.condition, alert.targetPrice);
47
+ }
48
+
16
49
  export function evaluateAlert(alert: AlertRule, currentPrice: number): boolean {
17
50
  if (alert.status !== "active") return false;
18
51
 
@@ -148,13 +148,14 @@ function storedAlerts(): AlertRule[] {
148
148
  return JSON.parse(value);
149
149
  }
150
150
 
151
- afterEach(() => {
151
+ afterEach(async () => {
152
152
  harnessState = null;
153
153
  harnessDispatch = null;
154
- if (testSetup) {
155
- testSetup.renderer.destroy();
156
- testSetup = undefined;
157
- }
154
+ if (!testSetup) return;
155
+ await act(async () => {
156
+ testSetup!.renderer.destroy();
157
+ });
158
+ testSetup = undefined;
158
159
  });
159
160
 
160
161
  describe("AlertsPane", () => {
@@ -179,6 +180,7 @@ describe("AlertsPane", () => {
179
180
  expect(frame).toContain("AAPL");
180
181
  expect(frame).toContain("MSFT");
181
182
  expect(frame).toContain("[a]dd alert");
183
+ expect(frame).toContain("[e]dit");
182
184
  expect(frame).toContain("[d]elete");
183
185
  expect(frame).not.toContain("Add Alert");
184
186
  expect(frame).not.toContain("Enter");
@@ -209,6 +211,45 @@ describe("AlertsPane", () => {
209
211
  expect(frame).toContain("[a]dd alert");
210
212
  });
211
213
 
214
+ test("edits the selected alert through the prefilled edit dialog", async () => {
215
+ testSetup = await testRender(
216
+ <AlertsHarness alerts={[makeAlert("alert-aapl", "AAPL", "above", 200)]} />,
217
+ { width: 110, height: 12 },
218
+ );
219
+
220
+ await renderSettled();
221
+ await act(async () => {
222
+ await testSetup!.mockInput.typeText("e");
223
+ await testSetup!.renderOnce();
224
+ await testSetup!.renderOnce();
225
+ });
226
+
227
+ const dialogFrame = testSetup.captureCharFrame();
228
+ expect(dialogFrame).toContain("Edit alert");
229
+ expect(dialogFrame).toContain("AAPL above 200");
230
+
231
+ await act(async () => {
232
+ for (const _ of "AAPL above 200") testSetup!.mockInput.pressBackspace();
233
+ await testSetup!.mockInput.typeText("MSFT crosses 310");
234
+ await testSetup!.renderOnce();
235
+ });
236
+
237
+ await act(async () => {
238
+ testSetup!.mockInput.pressEnter();
239
+ await testSetup!.renderOnce();
240
+ await testSetup!.renderOnce();
241
+ });
242
+
243
+ expect(storedAlerts()).toEqual([{
244
+ id: "alert-aapl",
245
+ symbol: "MSFT",
246
+ condition: "crosses",
247
+ targetPrice: 310,
248
+ createdAt: 1_700_000_000_000,
249
+ status: "active",
250
+ }]);
251
+ });
252
+
212
253
  test("opens the shared alert workflow from keyboard and mouse", async () => {
213
254
  const workflowCalls: string[] = [];
214
255
  const runtime = makeRuntime({
@@ -229,7 +270,9 @@ describe("AlertsPane", () => {
229
270
  });
230
271
  expect(workflowCalls).toEqual(["set-alert"]);
231
272
 
232
- testSetup.renderer.destroy();
273
+ await act(async () => {
274
+ testSetup!.renderer.destroy();
275
+ });
233
276
  testSetup = await testRender(<AlertsHarness alerts={[]} runtime={runtime} />, {
234
277
  width: 110,
235
278
  height: 12,
@@ -102,13 +102,14 @@ export const alertsPlugin: GloomPlugin = {
102
102
 
103
103
  // One batched pass over the distinct symbols: the alerts pane reads the same
104
104
  // persisted store, so this is the only place that talks to the provider.
105
- const symbols = [...new Set(activeAlerts.map((alert) => alert.symbol))];
106
- const results = await Promise.all(symbols.map(async (symbol): Promise<[string, Quote | string]> => {
105
+ const quoteKeys = [...new Set(activeAlerts.map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
106
+ const results = await Promise.all(quoteKeys.map(async (key): Promise<[string, Quote | string]> => {
107
+ const [symbol, exchange = ""] = key.split("\0");
107
108
  try {
108
- return [symbol, await resolveAlertQuote(ctx.marketData, symbol)];
109
+ return [key, await resolveAlertQuote(ctx.marketData, symbol ?? "", exchange)];
109
110
  } catch (err) {
110
- ctx.log.warn("poll: no quote", { symbol, error: String(err) });
111
- return [symbol, createQuoteErrorMessage(symbol, err)];
111
+ ctx.log.warn("poll: no quote", { symbol, exchange, error: String(err) });
112
+ return [key, createQuoteErrorMessage(symbol ?? "", err)];
112
113
  }
113
114
  }));
114
115
  const quotes = new Map<string, Quote | string>(results);
@@ -116,7 +117,7 @@ export const alertsPlugin: GloomPlugin = {
116
117
  let changed = false;
117
118
  for (const alert of alerts) {
118
119
  if (alert.status !== "active") continue;
119
- const quote = quotes.get(alert.symbol);
120
+ const quote = quotes.get(`${alert.symbol}\0${alert.exchange ?? ""}`);
120
121
  if (quote === undefined) continue;
121
122
  if (typeof quote === "string") {
122
123
  Object.assign(alert, quoteErrorAlertFields(quote));
@@ -134,6 +135,10 @@ export const alertsPlugin: GloomPlugin = {
134
135
  desktop: "always",
135
136
  persistent: true,
136
137
  sound: "Glass",
138
+ action: {
139
+ label: "Open",
140
+ onClick: () => ctx.showPane("alerts"),
141
+ },
137
142
  });
138
143
  }
139
144
  Object.assign(alert, quoteAlertFields(quote));
@@ -6,15 +6,20 @@ import {
6
6
  type DataTableColumn,
7
7
  type DataTableKeyEvent,
8
8
  } from "../../../components";
9
+ import { TextFieldDialog } from "../../../components/pane-settings-dialog/field-dialogs";
9
10
  import { colors } from "../../../theme/colors";
10
11
  import { TextAttributes } from "../../../ui";
12
+ import { useDialog, type AlertContext } from "../../../ui/dialog";
11
13
  import type { PaneProps } from "../../../types/plugin";
12
14
  import { usePluginAppActions, usePluginConfigState } from "../../runtime";
13
15
  import {
14
16
  deserializeAlerts,
17
+ editAlert,
18
+ rearmAlert as rebuildAlert,
15
19
  readAlertsStoreError,
16
20
  serializeAlerts,
17
21
  } from "./alert-engine";
22
+ import { parseAlertCommandValues } from "./command";
18
23
  import { ALERTS_KEY } from "./constants";
19
24
  import {
20
25
  conditionLabel,
@@ -59,6 +64,7 @@ const ALERT_TABLE_CONTENT_WIDTH = ALERT_COLUMNS.reduce(
59
64
  export function AlertsPane({ focused, width, height, close }: PaneProps) {
60
65
  const [alertsJson, setAlertsJson] = usePluginConfigState<string>(ALERTS_KEY, "[]");
61
66
  const { openPluginCommandWorkflow } = usePluginAppActions();
67
+ const dialog = useDialog();
62
68
  const [selectedIdx, setSelectedIdx] = useState(0);
63
69
  const showHorizontalScrollbar = ALERT_TABLE_CONTENT_WIDTH > width;
64
70
  const storeError = useMemo(() => readAlertsStoreError(alertsJson), [alertsJson]);
@@ -92,14 +98,7 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
92
98
 
93
99
  const rearmAlert = useCallback((id: string) => {
94
100
  savePaneAlerts(
95
- alerts.map((a) =>
96
- a.id === id ? {
97
- ...a,
98
- status: "active" as const,
99
- triggeredAt: undefined,
100
- lastCheckError: undefined,
101
- } : a,
102
- ),
101
+ alerts.map((a) => (a.id === id ? rebuildAlert(a) : a)),
103
102
  );
104
103
  }, [alerts, savePaneAlerts]);
105
104
 
@@ -112,6 +111,36 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
112
111
  if (selected) deleteAlert(selected.id);
113
112
  }, [deleteAlert, rows, selectedIdx]);
114
113
 
114
+ const editSelectedAlert = useCallback(() => {
115
+ const selected = rows[selectedIdx];
116
+ if (!selected) return;
117
+ void dialog.alert({
118
+ closeOnClickOutside: true,
119
+ content: (context: AlertContext) => (
120
+ <TextFieldDialog
121
+ {...context}
122
+ field={{
123
+ type: "text",
124
+ key: "alert",
125
+ label: "Edit alert",
126
+ description: "SYMBOL above|below|crosses PRICE",
127
+ placeholder: "AAPL above 200",
128
+ }}
129
+ currentValue={`${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
130
+ onApply={async (value) => {
131
+ const parsed = parseAlertCommandValues({ shortcut: value });
132
+ if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
133
+ savePaneAlerts((current) => current.map((alert) => (
134
+ alert.id === selected.id
135
+ ? editAlert(alert, parsed.symbol, parsed.condition, parsed.price)
136
+ : alert
137
+ )));
138
+ }}
139
+ />
140
+ ),
141
+ });
142
+ }, [dialog, rows, savePaneAlerts, selectedIdx]);
143
+
115
144
  // Quotes come from the plugin's single background poll, which writes into the
116
145
  // same persisted store, so the pane never fetches on its own.
117
146
 
@@ -123,6 +152,13 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
123
152
  : [],
124
153
  hints: [
125
154
  { id: "add", key: "a", label: "dd alert", onPress: startAddAlert },
155
+ {
156
+ id: "edit",
157
+ key: "e",
158
+ label: "dit",
159
+ onPress: editSelectedAlert,
160
+ disabled: rows.length === 0,
161
+ },
126
162
  {
127
163
  id: "delete",
128
164
  key: "d",
@@ -133,6 +169,7 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
133
169
  ],
134
170
  }), [
135
171
  deleteSelectedAlert,
172
+ editSelectedAlert,
136
173
  quoteError,
137
174
  rows.length,
138
175
  startAddAlert,
@@ -154,13 +191,18 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
154
191
  startAddAlert();
155
192
  return true;
156
193
  }
194
+ if (event.name === "e") {
195
+ event.preventDefault?.();
196
+ editSelectedAlert();
197
+ return true;
198
+ }
157
199
  if (event.name === "escape") {
158
200
  event.preventDefault?.();
159
201
  close?.();
160
202
  return true;
161
203
  }
162
204
  return false;
163
- }, [close, deleteSelectedAlert, startAddAlert]);
205
+ }, [close, deleteSelectedAlert, editSelectedAlert, startAddAlert]);
164
206
 
165
207
  const renderCell = useCallback((
166
208
  alert: AlertRule,