gloomberb 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -14
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -2,7 +2,6 @@ import { saveConfig } from "../../data/config-store";
2
2
  import { findPaneInstance, type LayoutConfig } from "../../types/config";
3
3
  import type { AppNotificationRequest, GloomPlugin, GloomPluginContext } from "../../types/plugin";
4
4
  import type { AppAction } from "../../state/app-context";
5
- import { getSharedRegistry } from "../registry";
6
5
  import { notifyGridlockComplete } from "../gridlock-notification";
7
6
  import {
8
7
  dockPane,
@@ -31,13 +30,11 @@ export function clearLayoutManagerDispatch() {
31
30
  getStateRef = null;
32
31
  }
33
32
 
34
- function persistLayout(layout: LayoutConfig) {
33
+ function persistLayout(ctx: Pick<GloomPluginContext, "getConfig">, layout: LayoutConfig) {
35
34
  if (!dispatchRef) return;
36
35
  dispatchRef({ type: "PUSH_LAYOUT_HISTORY" });
37
36
  dispatchRef({ type: "UPDATE_LAYOUT", layout });
38
- const registry = getSharedRegistry();
39
- if (!registry) return;
40
- const config = registry.getConfigFn();
37
+ const config = ctx.getConfig();
41
38
  const layouts = config.layouts.map((savedLayout, index) => (
42
39
  index === config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
43
40
  ));
@@ -67,8 +64,6 @@ export const layoutManagerPlugin: GloomPlugin = {
67
64
  category: "config",
68
65
  execute: async () => {
69
66
  if (!getStateRef) return;
70
- const registry = getSharedRegistry();
71
- if (!registry) return;
72
67
 
73
68
  const { layout, termWidth, termHeight, focusedPaneId } = getStateRef();
74
69
  const focusedPane = getFocusedPane(layout, focusedPaneId);
@@ -77,9 +72,9 @@ export const layoutManagerPlugin: GloomPlugin = {
77
72
  return;
78
73
  }
79
74
 
80
- const def = registry.panes.get(focusedPane.paneId);
75
+ const def = ctx.getPaneDef(focusedPane.paneId);
81
76
  const nextLayout = floatPane(layout, focusedPane.instanceId, termWidth, termHeight, def);
82
- persistLayout(nextLayout);
77
+ persistLayout(ctx, nextLayout);
83
78
  dispatchRef?.({ type: "FOCUS_PANE", paneId: focusedPane.instanceId });
84
79
  },
85
80
  });
@@ -92,8 +87,6 @@ export const layoutManagerPlugin: GloomPlugin = {
92
87
  category: "config",
93
88
  execute: async () => {
94
89
  if (!getStateRef) return;
95
- const registry = getSharedRegistry();
96
- if (!registry) return;
97
90
 
98
91
  const { layout, focusedPaneId } = getStateRef();
99
92
  const focusedPane = getFocusedPane(layout, focusedPaneId);
@@ -103,24 +96,11 @@ export const layoutManagerPlugin: GloomPlugin = {
103
96
  }
104
97
 
105
98
  const nextLayout = dockPane(layout, focusedPane.instanceId);
106
- persistLayout(nextLayout);
99
+ persistLayout(ctx, nextLayout);
107
100
  dispatchRef?.({ type: "FOCUS_PANE", paneId: focusedPane.instanceId });
108
101
  },
109
102
  });
110
103
 
111
- ctx.registerCommand({
112
- id: "add-pane",
113
- label: "New Pane",
114
- description: "Create a new pane from plugin templates",
115
- keywords: ["new", "add", "pane", "panel", "show"],
116
- category: "config",
117
- hidden: () => true,
118
- execute: async () => {
119
- ctx.openCommandBar("NP ");
120
- notify("Choose a pane template to create", { type: "info" });
121
- },
122
- });
123
-
124
104
  ctx.registerCommand({
125
105
  id: "gridlock-all",
126
106
  label: "Gridlock All Windows",
@@ -131,7 +111,7 @@ export const layoutManagerPlugin: GloomPlugin = {
131
111
  execute: async () => {
132
112
  if (!getStateRef) return;
133
113
  const { layout, termWidth, termHeight } = getStateRef();
134
- persistLayout(gridlockAllPanes(layout, { x: 0, y: 0, width: termWidth, height: termHeight }));
114
+ persistLayout(ctx, gridlockAllPanes(layout, { x: 0, y: 0, width: termWidth, height: termHeight }));
135
115
  notifyGridlockComplete(ctx.notify, () => {
136
116
  dispatchRef?.({ type: "UNDO_LAYOUT" });
137
117
  });
@@ -152,7 +132,7 @@ export const layoutManagerPlugin: GloomPlugin = {
152
132
  notify("Focus a pane to remove it", { type: "info" });
153
133
  return;
154
134
  }
155
- persistLayout(removePane(layout, focusedPane.instanceId));
135
+ persistLayout(ctx, removePane(layout, focusedPane.instanceId));
156
136
  },
157
137
  });
158
138
 
@@ -181,9 +161,7 @@ export const layoutManagerPlugin: GloomPlugin = {
181
161
  keywords: ["delete", "remove", "layout", "preset"],
182
162
  category: "config",
183
163
  confirm: () => {
184
- const registry = getSharedRegistry();
185
- if (!registry) return null;
186
- const config = registry.getConfigFn();
164
+ const config = ctx.getConfig();
187
165
  const layout = config.layouts[config.activeLayoutIndex];
188
166
  if (!layout) return null;
189
167
  return {
@@ -195,9 +173,7 @@ export const layoutManagerPlugin: GloomPlugin = {
195
173
  };
196
174
  },
197
175
  execute: async () => {
198
- const registry = getSharedRegistry();
199
- if (!registry) return;
200
- const config = registry.getConfigFn();
176
+ const config = ctx.getConfig();
201
177
  if (config.layouts.length <= 1) {
202
178
  notify("Can't delete the only layout", { type: "error" });
203
179
  return;
@@ -222,9 +198,7 @@ export const layoutManagerPlugin: GloomPlugin = {
222
198
  notify("Name is required", { type: "error" });
223
199
  return;
224
200
  }
225
- const registry = getSharedRegistry();
226
- if (!registry) return;
227
- dispatchRef?.({ type: "RENAME_LAYOUT", index: registry.getConfigFn().activeLayoutIndex, name });
201
+ dispatchRef?.({ type: "RENAME_LAYOUT", index: ctx.getConfig().activeLayoutIndex, name });
228
202
  notify(`Layout renamed to "${name}"`, { type: "success" });
229
203
  },
230
204
  });
@@ -236,9 +210,7 @@ export const layoutManagerPlugin: GloomPlugin = {
236
210
  keywords: ["duplicate", "copy", "clone", "layout"],
237
211
  category: "config",
238
212
  execute: async () => {
239
- const registry = getSharedRegistry();
240
- if (!registry) return;
241
- dispatchRef?.({ type: "DUPLICATE_LAYOUT", index: registry.getConfigFn().activeLayoutIndex });
213
+ dispatchRef?.({ type: "DUPLICATE_LAYOUT", index: ctx.getConfig().activeLayoutIndex });
242
214
  notify("Layout duplicated", { type: "success" });
243
215
  },
244
216
  });
@@ -265,7 +237,7 @@ export const layoutManagerPlugin: GloomPlugin = {
265
237
 
266
238
  const others = dockedPaneIds.filter((instanceId) => instanceId !== focusedPane.instanceId);
267
239
  if (others.length === 1) {
268
- persistLayout(swapPanes(layout, focusedPane.instanceId, others[0]!));
240
+ persistLayout(ctx, swapPanes(layout, focusedPane.instanceId, others[0]!));
269
241
  return;
270
242
  }
271
243
 
@@ -1,12 +1,11 @@
1
+ import { Box } from "../../../ui";
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
3
- import { useKeyboard } from "@opentui/react";
4
- import { DataTable, type DataTableCell, type DataTableColumn } from "../../../components";
3
+ import { TextAttributes } from "../../../ui";
4
+ import { DataTableView, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../../components";
5
5
  import type { GloomPlugin, PaneProps } from "../../../types/plugin";
6
- import { colors, priceColor, blendHex } from "../../../theme/colors";
6
+ import { colors, priceColor } from "../../../theme/colors";
7
7
  import { formatCurrency, formatCompact, formatPercentRaw } from "../../../utils/format";
8
- import { usePluginTickerActions } from "../../plugin-runtime";
9
- import { getSharedDataProvider } from "../../registry";
8
+ import { useAssetData, usePluginTickerActions } from "../../plugin-runtime";
10
9
  import {
11
10
  fetchScreener,
12
11
  fetchTrending,
@@ -193,19 +192,17 @@ const INDEX_SHORT: Record<string, string> = {
193
192
  };
194
193
 
195
194
  export function MarketMoversPane({ focused, width, height }: PaneProps) {
195
+ const dataProvider = useAssetData();
196
196
  const { pinTicker } = usePluginTickerActions();
197
197
  const [activeTab, setActiveTab] = useState<TabId>("gainers");
198
198
  const [quotes, setQuotes] = useState<ScreenerQuote[]>([]);
199
199
  const [loading, setLoading] = useState(false);
200
200
  const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
201
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
202
201
  const [sortPreference, setSortPreference] = useState<MarketMoverSortPreference>(DEFAULT_SORT_PREFERENCE);
203
202
  const [summaryQuotes, setSummaryQuotes] = useState<MarketSummaryQuote[]>([]);
204
203
 
205
204
  const cacheRef = useRef<Map<TabId, TabCache>>(new Map());
206
205
  const fetchGenRef = useRef(0);
207
- const scrollRef = useRef<ScrollBoxRenderable>(null);
208
- const headerScrollRef = useRef<ScrollBoxRenderable>(null);
209
206
 
210
207
  const columns = useMemo(() => buildColumns(width), [width]);
211
208
  const rankedRows = useMemo(() => createRows(quotes), [quotes]);
@@ -215,30 +212,6 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
215
212
  : -1;
216
213
  const activeIdx = selectedIdx >= 0 ? selectedIdx : (rows.length > 0 ? 0 : -1);
217
214
 
218
- const syncHeaderScroll = useCallback(() => {
219
- const bodyScrollBox = scrollRef.current;
220
- const headerScrollBox = headerScrollRef.current;
221
- if (bodyScrollBox && headerScrollBox && headerScrollBox.scrollLeft !== bodyScrollBox.scrollLeft) {
222
- headerScrollBox.scrollLeft = bodyScrollBox.scrollLeft;
223
- }
224
- }, []);
225
-
226
- const handleBodyScrollActivity = useCallback(() => {
227
- syncHeaderScroll();
228
- }, [syncHeaderScroll]);
229
-
230
- const resetTableScroll = useCallback(() => {
231
- const bodyScrollBox = scrollRef.current;
232
- if (bodyScrollBox) {
233
- bodyScrollBox.scrollTop = 0;
234
- bodyScrollBox.scrollLeft = 0;
235
- }
236
- const headerScrollBox = headerScrollRef.current;
237
- if (headerScrollBox) {
238
- headerScrollBox.scrollLeft = 0;
239
- }
240
- }, []);
241
-
242
215
  useEffect(() => {
243
216
  if (selectedSymbol && selectedIdx >= 0) return;
244
217
  const firstRow = rows[0];
@@ -249,16 +222,15 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
249
222
  }
250
223
  }, [rows, selectedIdx, selectedSymbol]);
251
224
 
252
- // Fetch market summary via the provider router
225
+ // Fetch market summary via the asset-data client.
253
226
  useEffect(() => {
254
- const provider = getSharedDataProvider();
255
- if (!provider) return;
227
+ if (!dataProvider) return;
256
228
  const loadSummary = async () => {
257
229
  const results: MarketSummaryQuote[] = [];
258
230
  await Promise.allSettled(
259
231
  MARKET_SUMMARY_SYMBOLS.map(async (symbol) => {
260
232
  try {
261
- const q = await provider.getQuote(symbol, "");
233
+ const q = await dataProvider.getQuote(symbol, "");
262
234
  if (q) {
263
235
  results.push({
264
236
  symbol,
@@ -281,14 +253,13 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
281
253
  loadSummary();
282
254
  const interval = setInterval(loadSummary, 60_000);
283
255
  return () => clearInterval(interval);
284
- }, []);
256
+ }, [dataProvider]);
285
257
 
286
- const loadTab = async (tab: TabId) => {
258
+ const loadTab = useCallback(async (tab: TabId) => {
287
259
  const cached = cacheRef.current.get(tab);
288
260
  if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
289
261
  setQuotes(cached.data);
290
262
  setSelectedSymbol(null);
291
- resetTableScroll();
292
263
  return;
293
264
  }
294
265
 
@@ -303,14 +274,13 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
303
274
  const trending = await fetchTrending(25);
304
275
  if (fetchGenRef.current !== gen) return;
305
276
 
306
- const provider = getSharedDataProvider();
307
277
  const resolved: ScreenerQuote[] = [];
308
278
 
309
- if (provider) {
279
+ if (dataProvider) {
310
280
  await Promise.allSettled(
311
281
  trending.slice(0, 25).map(async ({ symbol }) => {
312
282
  try {
313
- const q = await provider.getQuote(symbol, "");
283
+ const q = await dataProvider.getQuote(symbol, "");
314
284
  if (fetchGenRef.current !== gen) return;
315
285
  if (q) {
316
286
  resolved.push({
@@ -348,78 +318,50 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
348
318
  cacheRef.current.set(tab, { data, fetchedAt: Date.now() });
349
319
  setQuotes(data);
350
320
  setSelectedSymbol(null);
351
- resetTableScroll();
352
321
  } catch { /* leave existing data */ }
353
322
  finally {
354
323
  if (fetchGenRef.current === gen) setLoading(false);
355
324
  }
356
- };
325
+ }, [dataProvider]);
357
326
 
358
- useEffect(() => { loadTab(activeTab); }, [activeTab]);
327
+ useEffect(() => { loadTab(activeTab); }, [activeTab, loadTab]);
359
328
 
360
329
  const openSymbol = useCallback((symbol: string) => {
361
330
  pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
362
331
  }, [pinTicker]);
363
332
 
364
- const openSelected = useCallback((idx: number) => {
365
- const row = rows[idx];
366
- if (!row) return;
367
- openSymbol(row.symbol);
368
- }, [openSymbol, rows]);
369
-
370
333
  const handleHeaderClick = useCallback((columnId: string) => {
371
334
  setSortPreference((current) => nextSortPreference(current, columnId));
372
335
  }, []);
373
336
 
374
- useKeyboard((event) => {
375
- if (!focused) return;
376
-
337
+ const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
377
338
  const key = event.name;
378
- const isEnter = key === "enter" || key === "return";
379
339
 
380
340
  if (key === "tab" || key === "right" || key === "l") {
381
341
  event.preventDefault?.();
342
+ event.stopPropagation?.();
382
343
  const currentTabIdx = TABS.findIndex((t) => t.id === activeTab);
383
344
  setActiveTab(TABS[(currentTabIdx + 1) % TABS.length]!.id);
384
345
  setSelectedSymbol(null);
385
- resetTableScroll();
386
- } else if (key === "left" || key === "h") {
346
+ return true;
347
+ }
348
+ if (key === "left" || key === "h") {
387
349
  event.preventDefault?.();
350
+ event.stopPropagation?.();
388
351
  const currentTabIdx = TABS.findIndex((t) => t.id === activeTab);
389
352
  setActiveTab(TABS[(currentTabIdx - 1 + TABS.length) % TABS.length]!.id);
390
353
  setSelectedSymbol(null);
391
- resetTableScroll();
392
- } else if (key === "j" || key === "down") {
393
- event.preventDefault?.();
394
- if (rows.length === 0) return;
395
- const nextIdx = activeIdx >= 0 ? Math.min(activeIdx + 1, rows.length - 1) : 0;
396
- setSelectedSymbol(rows[nextIdx]!.symbol);
397
- } else if (key === "k" || key === "up") {
398
- event.preventDefault?.();
399
- if (rows.length === 0) return;
400
- const nextIdx = activeIdx > 0 ? activeIdx - 1 : 0;
401
- setSelectedSymbol(rows[nextIdx]!.symbol);
402
- } else if (isEnter) {
403
- event.preventDefault?.();
404
- openSelected(activeIdx);
405
- } else if (key === "r") {
354
+ return true;
355
+ }
356
+ if (key === "r") {
406
357
  event.preventDefault?.();
358
+ event.stopPropagation?.();
407
359
  cacheRef.current.delete(activeTab);
408
360
  loadTab(activeTab);
361
+ return true;
409
362
  }
410
- });
411
-
412
- // Scroll to keep selected row visible
413
- useEffect(() => {
414
- const sb = scrollRef.current;
415
- if (!sb?.viewport || rows.length === 0 || activeIdx < 0) return;
416
- const viewportHeight = Math.max(sb.viewport.height, 1);
417
- if (activeIdx < sb.scrollTop) {
418
- sb.scrollTo(activeIdx);
419
- } else if (activeIdx >= sb.scrollTop + viewportHeight) {
420
- sb.scrollTo(activeIdx - viewportHeight + 1);
421
- }
422
- }, [activeIdx, rows.length]);
363
+ return false;
364
+ }, [activeTab, loadTab]);
423
365
 
424
366
  const renderCell = useCallback((
425
367
  row: MarketMoverRow,
@@ -467,69 +409,53 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
467
409
  }
468
410
  }, []);
469
411
 
470
- const summaryBg = blendHex(colors.bg, colors.border, 0.2);
412
+ const refreshActiveTab = useCallback(() => {
413
+ cacheRef.current.delete(activeTab);
414
+ void loadTab(activeTab);
415
+ }, [activeTab, loadTab]);
471
416
 
472
- return (
473
- <box flexDirection="column" width={width} height={height}>
474
- {/* Market Summary Bar */}
475
- {summaryQuotes.length > 0 ? (
476
- <box flexDirection="row" height={1} backgroundColor={summaryBg} paddingX={1} gap={2}>
477
- {summaryQuotes.map((idx) => {
478
- const short = INDEX_SHORT[idx.symbol] ?? idx.symbol;
479
- const chgColor = priceColor(idx.changePercent);
480
- return (
481
- <box key={idx.symbol} flexDirection="row" gap={1}>
482
- <text fg={colors.textBright} attributes={TextAttributes.BOLD}>{short}</text>
483
- <text fg={colors.text}>{formatCurrency(idx.price, "USD")}</text>
484
- <text fg={chgColor}>{formatPercentRaw(idx.changePercent)}</text>
485
- </box>
486
- );
487
- })}
488
- {loading && <text fg={colors.textMuted}> loading…</text>}
489
- </box>
490
- ) : null}
491
-
492
- {/* Tab bar */}
493
- <box flexDirection="row" height={1} paddingX={1}>
494
- {TABS.map((tab) => {
495
- const isActive = tab.id === activeTab;
496
- return (
497
- <box
498
- key={tab.id}
499
- paddingX={1}
500
- marginRight={1}
501
- onMouseDown={(event: any) => {
502
- event.preventDefault?.();
503
- setActiveTab(tab.id);
504
- setSelectedSymbol(null);
505
- resetTableScroll();
506
- }}
507
- >
508
- <text
509
- fg={isActive ? colors.textBright : colors.textDim}
510
- attributes={isActive ? TextAttributes.BOLD | TextAttributes.UNDERLINE : TextAttributes.NONE}
511
- >
512
- {tab.label}
513
- </text>
514
- </box>
515
- );
516
- })}
517
- <box flexGrow={1} />
518
- <text fg={colors.textMuted}>{quotes.length} stocks</text>
519
- </box>
417
+ usePaneFooter("market-movers", () => ({
418
+ info: [
419
+ ...summaryQuotes.map((idx) => {
420
+ const short = INDEX_SHORT[idx.symbol] ?? idx.symbol;
421
+ return {
422
+ id: `summary:${idx.symbol}`,
423
+ parts: [
424
+ { text: short, tone: "label" as const },
425
+ { text: formatPercentRaw(idx.changePercent), tone: "value" as const, color: priceColor(idx.changePercent), bold: true },
426
+ ],
427
+ };
428
+ }),
429
+ ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
430
+ ],
431
+ hints: [{ id: "refresh", key: "r", label: "efresh", onPress: refreshActiveTab }],
432
+ }), [loading, refreshActiveTab, summaryQuotes]);
520
433
 
521
- <DataTable<MarketMoverRow, MarketMoverColumn>
434
+ return (
435
+ <Box flexDirection="column" width={width} height={height}>
436
+ <Box height={1} paddingX={1}>
437
+ <Tabs
438
+ tabs={TABS.map((tab) => ({ label: tab.label, value: tab.id }))}
439
+ activeValue={activeTab}
440
+ onSelect={(value) => {
441
+ setActiveTab(value as TabId);
442
+ setSelectedSymbol(null);
443
+ }}
444
+ compact
445
+ variant="bare"
446
+ />
447
+ </Box>
448
+
449
+ <DataTableView<MarketMoverRow, MarketMoverColumn>
450
+ focused={focused}
451
+ selectedIndex={activeIdx}
452
+ onRootKeyDown={handleTableKeyDown}
453
+ resetScrollKey={activeTab}
522
454
  columns={columns}
523
455
  items={rows}
524
456
  sortColumnId={sortPreference.columnId}
525
457
  sortDirection={sortPreference.direction}
526
458
  onHeaderClick={handleHeaderClick}
527
- headerScrollRef={headerScrollRef}
528
- scrollRef={scrollRef}
529
- syncHeaderScroll={syncHeaderScroll}
530
- onBodyScrollActivity={handleBodyScrollActivity}
531
- hoveredIdx={hoveredIdx}
532
- setHoveredIdx={setHoveredIdx}
533
459
  getItemKey={(row) => `${row.symbol}-${row.rank}`}
534
460
  isSelected={(row) => row.symbol === selectedSymbol}
535
461
  onSelect={(row) => setSelectedSymbol(row.symbol)}
@@ -537,7 +463,7 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
537
463
  renderCell={renderCell}
538
464
  emptyStateTitle={loading ? "Loading movers..." : "No data"}
539
465
  />
540
- </box>
466
+ </Box>
541
467
  );
542
468
  }
543
469
 
@@ -567,7 +493,7 @@ export const marketMoversPlugin: GloomPlugin = {
567
493
  label: "Market Movers",
568
494
  description: "Top gainers, losers, most active, and trending tickers.",
569
495
  keywords: ["movers", "gainers", "losers", "active", "trending", "screener", "top"],
570
- shortcut: { prefix: "TOP" },
496
+ shortcut: { prefix: "MOST" },
571
497
  },
572
498
  ],
573
499
  };
@@ -82,18 +82,6 @@ describe("parseScreenerResponse", () => {
82
82
  expect(msft.volumeRatio).toBeCloseTo(0.72, 1);
83
83
  });
84
84
 
85
- test("returns [] for missing finance.result", () => {
86
- expect(parseScreenerResponse({})).toEqual([]);
87
- expect(parseScreenerResponse(null)).toEqual([]);
88
- expect(parseScreenerResponse({ finance: {} })).toEqual([]);
89
- expect(parseScreenerResponse({ finance: { result: [] } })).toEqual([]);
90
- });
91
-
92
- test("returns [] for non-array quotes", () => {
93
- expect(parseScreenerResponse({ finance: { result: [{ quotes: null }] } })).toEqual([]);
94
- expect(parseScreenerResponse({ finance: { result: [{}] } })).toEqual([]);
95
- });
96
-
97
85
  test("skips entries without a string symbol", () => {
98
86
  const data = {
99
87
  finance: {
@@ -124,12 +112,6 @@ describe("parseTrendingResponse", () => {
124
112
  expect(results[1]!.symbol).toBe("TSLA");
125
113
  });
126
114
 
127
- test("returns [] for malformed input", () => {
128
- expect(parseTrendingResponse({})).toEqual([]);
129
- expect(parseTrendingResponse(null)).toEqual([]);
130
- expect(parseTrendingResponse({ finance: { result: [{ quotes: "bad" }] } })).toEqual([]);
131
- });
132
-
133
115
  test("skips entries without a string symbol", () => {
134
116
  const data = {
135
117
  finance: {
@@ -0,0 +1,98 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { AppNotificationRequest, GloomPluginContext } from "../../../types/plugin";
3
+ import type { MarketNewsItem, NewsQueryState } from "../../../types/news-source";
4
+ import {
5
+ BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY,
6
+ setupBreakingNewsNotifications,
7
+ } from "./breaking-notifications";
8
+
9
+ function article(id: string, title = id): MarketNewsItem {
10
+ return {
11
+ id,
12
+ title,
13
+ url: `https://example.com/${id}`,
14
+ source: "Test Wire",
15
+ publishedAt: new Date(),
16
+ topic: "general",
17
+ topics: ["general"],
18
+ sectors: [],
19
+ categories: ["general"],
20
+ tickers: ["AAPL"],
21
+ scores: {
22
+ importance: 90,
23
+ urgency: 90,
24
+ marketImpact: 80,
25
+ novelty: 80,
26
+ confidence: 90,
27
+ },
28
+ importance: 90,
29
+ isBreaking: true,
30
+ isDeveloping: false,
31
+ };
32
+ }
33
+
34
+ function ready(articles: MarketNewsItem[]): NewsQueryState {
35
+ return {
36
+ phase: "ready",
37
+ articles,
38
+ error: null,
39
+ updatedAt: Date.now(),
40
+ sourceIds: ["test"],
41
+ };
42
+ }
43
+
44
+ describe("breaking news notifications", () => {
45
+ test("primes on the first ready batch and notifies for later unseen articles", () => {
46
+ let enabled = true;
47
+ let listener: ((state: NewsQueryState) => void) | null = null;
48
+ const notifications: AppNotificationRequest[] = [];
49
+ const shownPanes: string[] = [];
50
+
51
+ const ctx = {
52
+ configState: {
53
+ get: (key: string) => key === BREAKING_NEWS_NOTIFICATIONS_ENABLED_KEY ? enabled : null,
54
+ },
55
+ watchNewsQuery: (_query: unknown, nextListener: (state: NewsQueryState) => void) => {
56
+ listener = nextListener;
57
+ return () => {
58
+ listener = null;
59
+ };
60
+ },
61
+ on: (_event: string, _handler: unknown) => () => {},
62
+ notify: (notification: AppNotificationRequest) => {
63
+ notifications.push(notification);
64
+ },
65
+ showPane: (paneId: string) => {
66
+ shownPanes.push(paneId);
67
+ },
68
+ log: {
69
+ warn: () => {},
70
+ },
71
+ } as unknown as GloomPluginContext;
72
+
73
+ const dispose = setupBreakingNewsNotifications(ctx);
74
+ const oldArticle = article("old", "Old headline");
75
+ const newArticle = article("new", "New headline");
76
+
77
+ listener?.(ready([oldArticle]));
78
+ listener?.(ready([newArticle, oldArticle]));
79
+
80
+ expect(notifications).toHaveLength(1);
81
+ expect(notifications[0]).toMatchObject({
82
+ title: "Breaking News",
83
+ body: "New headline",
84
+ desktop: "always",
85
+ persistent: true,
86
+ });
87
+
88
+ notifications[0]!.action?.onClick();
89
+ expect(shownPanes).toEqual(["news-breaking"]);
90
+
91
+ enabled = false;
92
+ listener?.(ready([article("newer", "Newer headline"), newArticle, oldArticle]));
93
+ expect(notifications).toHaveLength(1);
94
+
95
+ dispose();
96
+ expect(listener).toBeNull();
97
+ });
98
+ });