gloomberb 0.5.0 → 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 -13
  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 +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  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 +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  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 +186 -6
  440. package/src/utils/api-client.ts +433 -33
  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
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useReducer, type ReactElement } from "react";
3
- import { testRender } from "@opentui/react/test-utils";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
4
  import type { ScrollBoxRenderable } from "@opentui/core";
5
5
  import {
6
6
  AppContext,
@@ -17,13 +17,14 @@ import {
17
17
  type AppConfig,
18
18
  type BrokerInstanceConfig,
19
19
  } from "../../types/config";
20
+ import { createTestPluginRuntime } from "../../test-support/plugin-runtime";
20
21
  import type { DataProvider } from "../../types/data-provider";
21
22
  import type { TickerFinancials } from "../../types/financials";
22
23
  import type { DetailTabDef } from "../../types/plugin";
23
24
  import type { TickerRecord } from "../../types/ticker";
24
25
  import type { PluginRegistry } from "../registry";
25
26
  import { setSharedRegistryForTests } from "../registry";
26
- import { resetOptionsAvailabilityCache } from "./options-availability";
27
+ import { PluginRenderProvider } from "../plugin-runtime";
27
28
  import { FinancialsTab, tickerDetailPlugin } from "./ticker-detail";
28
29
  import { isUsEquityTicker } from "../../utils/sec";
29
30
 
@@ -33,6 +34,8 @@ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
33
34
  let harnessDispatch: React.Dispatch<AppAction> | null = null;
34
35
  let sharedCoordinator: MarketDataCoordinator | null = null;
35
36
 
37
+ const runtime = createTestPluginRuntime();
38
+
36
39
  const DetailPane = tickerDetailPlugin.panes![0]!.component as (props: {
37
40
  paneId: string;
38
41
  paneType: string;
@@ -136,8 +139,14 @@ function makeRegistry(): PluginRegistry {
136
139
  <text>stub</text>
137
140
  );
138
141
  const detailTabs = new Map<string, DetailTabDef>([
139
- ["ibkr-trade", { id: "ibkr-trade", name: "Trade", order: 25, component: stubTab }],
140
- ["options", { id: "options", name: "Options", order: 35, component: stubTab }],
142
+ ["ibkr-trade", {
143
+ id: "ibkr-trade",
144
+ name: "Trade",
145
+ order: 25,
146
+ component: stubTab,
147
+ isVisible: ({ config }) => config.brokerInstances.some((instance) => instance.brokerType === "ibkr" && instance.connectionMode === "gateway"),
148
+ }],
149
+ ["options", { id: "options", name: "Options", order: 35, component: stubTab, isVisible: ({ hasOptionsChain }) => hasOptionsChain }],
141
150
  ["sec", { id: "sec", name: "SEC", order: 45, component: stubTab, isVisible: ({ ticker }) => isUsEquityTicker(ticker) }],
142
151
  ["ai-chat", { id: "ai-chat", name: "Ask AI", order: 60, component: stubTab }],
143
152
  ]);
@@ -218,13 +227,15 @@ function DetailHarness({
218
227
  <AppContext value={{ state, dispatch }}>
219
228
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
220
229
  <text>{`active:${state.paneState[TEST_PANE_ID]?.activeTabId ?? ""}`}</text>
221
- <DetailPane
222
- paneId={TEST_PANE_ID}
223
- paneType="ticker-detail"
224
- focused
225
- width={width}
226
- height={height}
227
- />
230
+ <PluginRenderProvider pluginId={tickerDetailPlugin.id} runtime={runtime}>
231
+ <DetailPane
232
+ paneId={TEST_PANE_ID}
233
+ paneType="ticker-detail"
234
+ focused
235
+ width={width}
236
+ height={height}
237
+ />
238
+ </PluginRenderProvider>
228
239
  </PaneInstanceProvider>
229
240
  </AppContext>
230
241
  );
@@ -248,7 +259,6 @@ afterEach(() => {
248
259
  testSetup = undefined;
249
260
  }
250
261
  harnessDispatch = null;
251
- resetOptionsAvailabilityCache();
252
262
  setSharedRegistryForTests(undefined);
253
263
  setOptionsProvider(undefined);
254
264
  });
@@ -343,13 +353,7 @@ describe("FinancialsTab", () => {
343
353
  });
344
354
 
345
355
  describe("TickerDetailPane", () => {
346
- test("defaults new detail panes to floating", () => {
347
- const paneDef = tickerDetailPlugin.panes?.find((entry) => entry.id === "ticker-detail");
348
-
349
- expect(paneDef?.defaultMode).toBe("floating");
350
- });
351
-
352
- test("shows only applicable tabs when no gateway, statements, or options are available", async () => {
356
+ test("shows core and lightweight plugin tabs without waiting on options preflight", async () => {
353
357
  setSharedRegistryForTests(makeRegistry());
354
358
  setOptionsProvider(createProvider(false));
355
359
 
@@ -368,10 +372,10 @@ describe("TickerDetailPane", () => {
368
372
  const frame = testSetup.captureCharFrame();
369
373
  expect(frame).toContain("Overview");
370
374
  expect(frame).toContain("Chart");
375
+ expect(frame).toContain("Options");
371
376
  expect(frame).toContain("Ask AI");
372
377
  expect(frame).not.toContain("Financials");
373
378
  expect(frame).not.toContain("Trade");
374
- expect(frame).not.toContain("Options");
375
379
  });
376
380
 
377
381
  test("shows Financials when statement data exists", async () => {
@@ -412,7 +416,7 @@ describe("TickerDetailPane", () => {
412
416
  expect(frame).toContain("Trade");
413
417
  });
414
418
 
415
- test("shows Options after the preflight confirms a chain exists", async () => {
419
+ test("shows Options for option-capable tickers without a preflight round trip", async () => {
416
420
  setSharedRegistryForTests(makeRegistry());
417
421
  setOptionsProvider(createProvider(true));
418
422
 
@@ -450,6 +454,61 @@ describe("TickerDetailPane", () => {
450
454
  expect(frame).toContain("SEC");
451
455
  });
452
456
 
457
+ test("hides plugin tabs when their owner plugin is disabled", async () => {
458
+ setSharedRegistryForTests({
459
+ ...makeRegistry(),
460
+ getDetailTabPluginId: (tabId: string) => (
461
+ tabId === "sec" ? "company-research" : tabId
462
+ ),
463
+ } as unknown as PluginRegistry);
464
+ setOptionsProvider(createProvider(false));
465
+ const config = createDetailConfig("AAPL");
466
+ config.disabledPlugins = ["company-research"];
467
+
468
+ testSetup = await testRender(
469
+ <DetailHarness
470
+ config={config}
471
+ ticker={makeTicker("AAPL")}
472
+ financials={null}
473
+ />,
474
+ { width: 90, height: 24 },
475
+ );
476
+
477
+ await flushFrame();
478
+ const frame = testSetup.captureCharFrame();
479
+ expect(frame).not.toContain("SEC");
480
+ });
481
+
482
+ test("passes visible tab content height to plugin tabs", async () => {
483
+ let receivedHeight: number | null = null;
484
+ const probeTab: DetailTabDef["component"] = ({ height }) => {
485
+ receivedHeight = height;
486
+ return <text>{`height:${height}`}</text>;
487
+ };
488
+ setSharedRegistryForTests({
489
+ detailTabs: new Map<string, DetailTabDef>([
490
+ ["sec", { id: "sec", name: "SEC", order: 45, component: probeTab, isVisible: ({ ticker }) => isUsEquityTicker(ticker) }],
491
+ ]),
492
+ } as unknown as PluginRegistry);
493
+ setOptionsProvider(createProvider(false));
494
+
495
+ testSetup = await testRender(
496
+ <DetailHarness
497
+ config={createDetailConfig("AAPL")}
498
+ ticker={makeTicker("AAPL")}
499
+ financials={null}
500
+ activeTabId="sec"
501
+ height={18}
502
+ />,
503
+ { width: 90, height: 18 },
504
+ );
505
+
506
+ await flushFrame();
507
+ const frame = testSetup.captureCharFrame();
508
+ expect(receivedHeight).toBe(17);
509
+ expect(frame).toContain("height:17");
510
+ });
511
+
453
512
  test("hides SEC for non-US equities", async () => {
454
513
  setSharedRegistryForTests(makeRegistry());
455
514
  setOptionsProvider(createProvider(false));
@@ -495,7 +554,7 @@ describe("TickerDetailPane", () => {
495
554
  expect(frame).toContain("Builds widgets for industrial customers.");
496
555
  });
497
556
 
498
- test("shows listing venue, session, sources, and route separately in Overview", async () => {
557
+ test("shows listing venue, session, compact sources, and route in Overview", async () => {
499
558
  setSharedRegistryForTests(makeRegistry());
500
559
  setOptionsProvider(createProvider(false));
501
560
 
@@ -524,7 +583,7 @@ describe("TickerDetailPane", () => {
524
583
  preMarketChangePercent: 2,
525
584
  provenance: {
526
585
  price: { providerId: "ibkr", dataSource: "live" },
527
- session: { providerId: "yahoo", dataSource: "yahoo" },
586
+ session: { providerId: "yahoo", dataSource: "delayed" },
528
587
  },
529
588
  },
530
589
  })}
@@ -537,9 +596,64 @@ describe("TickerDetailPane", () => {
537
596
  const frame = testSetup.captureCharFrame();
538
597
  expect(frame).toContain("NASDAQ");
539
598
  expect(frame).toContain("PRE-MKT");
540
- expect(frame).toContain("Price source: IBKR live");
541
- expect(frame).toContain("Session source: Yahoo");
542
- expect(frame).toContain("Route: SMART");
599
+ expect(frame).toContain("src IBKR live/Yahoo");
600
+ expect(frame).toContain("route SMART");
601
+ });
602
+
603
+ test("renders compact quote book, spread percent, and range widgets without duplicate overview rows", async () => {
604
+ setSharedRegistryForTests(makeRegistry());
605
+ setOptionsProvider(createProvider(false));
606
+
607
+ testSetup = await testRender(
608
+ <DetailHarness
609
+ config={createDetailConfig("AMD")}
610
+ ticker={makeTicker("AMD", "Advanced Micro Devices")}
611
+ financials={makeFinancials({
612
+ quote: {
613
+ symbol: "AMD",
614
+ price: 100,
615
+ currency: "USD",
616
+ change: -1,
617
+ changePercent: -1,
618
+ previousClose: 101,
619
+ open: 99,
620
+ high: 102,
621
+ low: 98,
622
+ high52w: 130,
623
+ low52w: 80,
624
+ volume: 12_500_000,
625
+ marketCap: 250_000_000_000,
626
+ bid: 99.5,
627
+ ask: 100.5,
628
+ bidSize: 400,
629
+ askSize: 500,
630
+ lastUpdated: Date.now(),
631
+ },
632
+ fundamentals: {
633
+ sharesOutstanding: 1_000_000_000,
634
+ trailingPE: 25,
635
+ },
636
+ })}
637
+ width={110}
638
+ height={28}
639
+ />,
640
+ { width: 110, height: 28 },
641
+ );
642
+
643
+ await flushFrame();
644
+ const frame = testSetup.captureCharFrame();
645
+ expect(frame).toContain("Bid");
646
+ expect(frame).toContain("400 x $99.5");
647
+ expect(frame).toContain("Ask");
648
+ expect(frame).toContain("500 x $100.5");
649
+ expect(frame).toContain("Spr");
650
+ expect(frame).toContain("1.00%");
651
+ expect(frame).toContain("Day Range");
652
+ expect(frame).toContain("52W Range");
653
+ expect(frame).not.toContain("Prior Close");
654
+ expect(frame).toContain("Volume");
655
+ expect(frame).toContain("Market Cap");
656
+ expect(frame).toContain("P/E");
543
657
  });
544
658
 
545
659
  test("keeps quote prices native while converting market cap and position totals to base currency", async () => {
@@ -553,11 +667,11 @@ describe("TickerDetailPane", () => {
553
667
  exchange: "XETRA",
554
668
  currency: "EUR",
555
669
  positions: [{
556
- portfolio: "main",
670
+ portfolio: "broker:ibkr-interactive-brokers:UTEST12345",
557
671
  shares: 10,
558
672
  avgCost: 100,
559
673
  currency: "EUR",
560
- broker: "manual",
674
+ broker: "ibkr",
561
675
  markPrice: 125,
562
676
  marketValue: 1250,
563
677
  unrealizedPnl: 250,
@@ -588,11 +702,20 @@ describe("TickerDetailPane", () => {
588
702
  const frame = testSetup.captureCharFrame();
589
703
  expect(frame).toContain("€125");
590
704
  expect(frame).toContain("2.2B USD");
591
- expect(frame).toContain("@ €100");
592
- expect(frame).toContain("= $1,100.00");
593
- expect(frame).toContain("P&L: +$275.00");
594
- expect(frame).toContain("Mark: €125");
595
- expect(frame).toContain("Mkt Value: $1,375.00");
705
+ expect(frame).toContain("Account");
706
+ expect(frame).toContain("Qty");
707
+ expect(frame).toContain("Avg");
708
+ expect(frame).toContain("Cost");
709
+ expect(frame).toContain("Value");
710
+ expect(frame).toContain("Ret");
711
+ expect(frame).toContain("UTEST12345");
712
+ expect(frame).toContain("10 sh");
713
+ expect(frame).toContain("€100");
714
+ expect(frame).toContain("€125");
715
+ expect(frame).toContain("$1,100.00");
716
+ expect(frame).toContain("$1,375.00");
717
+ expect(frame).toContain("+$275.00");
718
+ expect(frame).toContain("+25.00%");
596
719
  });
597
720
 
598
721
  test("falls back to Overview when a hidden active tab becomes unavailable", async () => {
@@ -0,0 +1,68 @@
1
+ import type {
2
+ PaneTemplateContext,
3
+ PaneTemplateCreateOptions,
4
+ PaneTemplateDef,
5
+ PaneTemplateInstanceConfig,
6
+ } from "../../types/plugin";
7
+ import { normalizeTickerInput } from "../../utils/ticker-search";
8
+
9
+ export interface TickerSurfacePaneTemplateOptions {
10
+ id: string;
11
+ paneId: string;
12
+ label: string;
13
+ description: string;
14
+ keywords: string[];
15
+ shortcut: string;
16
+ titlePrefix?: string;
17
+ canCreate?: (
18
+ context: PaneTemplateContext,
19
+ options: PaneTemplateCreateOptions | undefined,
20
+ symbol: string,
21
+ ) => boolean;
22
+ }
23
+
24
+ export function resolveTickerSurfaceSymbol(
25
+ context: Pick<PaneTemplateContext, "activeTicker">,
26
+ options?: Pick<PaneTemplateCreateOptions, "arg" | "symbol">,
27
+ ): string | null {
28
+ const explicitSymbol = options?.symbol?.trim().toUpperCase();
29
+ return explicitSymbol || normalizeTickerInput(context.activeTicker, options?.arg);
30
+ }
31
+
32
+ export function createTickerSurfacePaneInstance(
33
+ context: Pick<PaneTemplateContext, "activeTicker">,
34
+ options: Pick<PaneTemplateCreateOptions, "arg" | "symbol"> | undefined,
35
+ titlePrefix: string,
36
+ ): PaneTemplateInstanceConfig | null {
37
+ const ticker = resolveTickerSurfaceSymbol(context, options);
38
+ return ticker
39
+ ? {
40
+ title: `${titlePrefix} ${ticker}`,
41
+ binding: { kind: "fixed", symbol: ticker },
42
+ placement: "floating",
43
+ }
44
+ : null;
45
+ }
46
+
47
+ export function createTickerSurfacePaneTemplate(
48
+ templateOptions: TickerSurfacePaneTemplateOptions,
49
+ ): PaneTemplateDef {
50
+ const titlePrefix = templateOptions.titlePrefix ?? templateOptions.shortcut;
51
+ return {
52
+ id: templateOptions.id,
53
+ paneId: templateOptions.paneId,
54
+ label: templateOptions.label,
55
+ description: templateOptions.description,
56
+ keywords: templateOptions.keywords,
57
+ shortcut: {
58
+ prefix: templateOptions.shortcut,
59
+ argPlaceholder: "ticker",
60
+ argKind: "ticker",
61
+ },
62
+ canCreate: (context, createOptions) => {
63
+ const symbol = resolveTickerSurfaceSymbol(context, createOptions);
64
+ return symbol !== null && (templateOptions.canCreate?.(context, createOptions, symbol) ?? true);
65
+ },
66
+ createInstance: (context, createOptions) => createTickerSurfacePaneInstance(context, createOptions, titlePrefix),
67
+ };
68
+ }
@@ -1,14 +1,12 @@
1
1
  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";
2
+ import { TextAttributes } from "../../../ui";
3
+ import { DataTableView, usePaneFooter, type DataTableCell, type DataTableColumn } from "../../../components";
5
4
  import type { GloomPlugin, PaneProps } from "../../../types/plugin";
6
5
  import type { Quote } from "../../../types/financials";
7
6
  import type { MarketState } from "../../../types/financials";
8
7
  import { colors, priceColor } from "../../../theme/colors";
9
8
  import { formatCurrency, formatPercentRaw } from "../../../utils/format";
10
- import { usePluginTickerActions } from "../../plugin-runtime";
11
- import { getSharedDataProvider } from "../../registry";
9
+ import { useAssetData, usePluginTickerActions } from "../../plugin-runtime";
12
10
  import { WORLD_INDICES, REGION_LABELS, REGION_ORDER, getIndicesByRegion, type IndexEntry } from "./indices";
13
11
 
14
12
  const REFRESH_INTERVAL_MS = 60_000;
@@ -151,14 +149,12 @@ function nextSortPreference(
151
149
  }
152
150
 
153
151
  export function WorldIndicesPane({ focused, width, height }: PaneProps) {
152
+ const dataProvider = useAssetData();
154
153
  const { pinTicker } = usePluginTickerActions();
155
154
  const [quotes, setQuotes] = useState<QuoteMap>(new Map());
156
155
  const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
157
- const [hoveredFlatIdx, setHoveredFlatIdx] = useState<number | null>(null);
158
156
  const [sortPreference, setSortPreference] = useState<WorldIndexSortPreference>(DEFAULT_SORT_PREFERENCE);
159
157
  const fetchGenRef = useRef(0);
160
- const scrollRef = useRef<ScrollBoxRenderable>(null);
161
- const headerScrollRef = useRef<ScrollBoxRenderable>(null);
162
158
 
163
159
  const indicesByRegion = useMemo(() => getIndicesByRegion(), []);
164
160
  const flatRows = useMemo(
@@ -181,9 +177,8 @@ export function WorldIndicesPane({ focused, width, height }: PaneProps) {
181
177
  }
182
178
  }, [flatRows, selectedFlatIdx, selectedSymbol]);
183
179
 
184
- const fetchAll = () => {
185
- const provider = getSharedDataProvider();
186
- if (!provider) return;
180
+ const fetchAll = useCallback(() => {
181
+ if (!dataProvider) return;
187
182
 
188
183
  fetchGenRef.current += 1;
189
184
  const gen = fetchGenRef.current;
@@ -196,7 +191,7 @@ export function WorldIndicesPane({ focused, width, height }: PaneProps) {
196
191
  return next;
197
192
  });
198
193
 
199
- provider.getQuote(entry.symbol, "").then((quote) => {
194
+ dataProvider.getQuote(entry.symbol, "").then((quote) => {
200
195
  if (fetchGenRef.current !== gen) return;
201
196
  setQuotes((prev) => {
202
197
  const next = new Map(prev);
@@ -213,25 +208,13 @@ export function WorldIndicesPane({ focused, width, height }: PaneProps) {
213
208
  });
214
209
  });
215
210
  }
216
- };
211
+ }, [dataProvider]);
217
212
 
218
213
  useEffect(() => {
219
214
  fetchAll();
220
215
  const interval = setInterval(fetchAll, REFRESH_INTERVAL_MS);
221
216
  return () => clearInterval(interval);
222
- }, []);
223
-
224
- const syncHeaderScroll = useCallback(() => {
225
- const bodyScrollBox = scrollRef.current;
226
- const headerScrollBox = headerScrollRef.current;
227
- if (bodyScrollBox && headerScrollBox && headerScrollBox.scrollLeft !== bodyScrollBox.scrollLeft) {
228
- headerScrollBox.scrollLeft = bodyScrollBox.scrollLeft;
229
- }
230
- }, []);
231
-
232
- const handleBodyScrollActivity = useCallback(() => {
233
- syncHeaderScroll();
234
- }, [syncHeaderScroll]);
217
+ }, [fetchAll]);
235
218
 
236
219
  const openSelected = useCallback((flatIdx: number) => {
237
220
  const row = flatRows[flatIdx];
@@ -249,38 +232,6 @@ export function WorldIndicesPane({ focused, width, height }: PaneProps) {
249
232
  setSortPreference((current) => nextSortPreference(current, columnId));
250
233
  }, []);
251
234
 
252
- useKeyboard((event) => {
253
- if (!focused) return;
254
-
255
- const currentPos = navigableIndices.indexOf(activeFlatIdx);
256
- const key = event.name;
257
- const isEnter = key === "enter" || key === "return";
258
-
259
- if (key === "j" || key === "down") {
260
- event.preventDefault?.();
261
- const next = navigableIndices[currentPos >= 0 ? currentPos + 1 : 0];
262
- if (next !== undefined) selectFlatIndex(next);
263
- } else if (key === "k" || key === "up") {
264
- event.preventDefault?.();
265
- const next = navigableIndices[currentPos > 0 ? currentPos - 1 : 0];
266
- if (next !== undefined) selectFlatIndex(next);
267
- } else if (isEnter) {
268
- event.preventDefault?.();
269
- openSelected(activeFlatIdx);
270
- }
271
- });
272
-
273
- useEffect(() => {
274
- const scrollBox = scrollRef.current;
275
- if (!scrollBox?.viewport || activeFlatIdx < 0) return;
276
- const viewportHeight = Math.max(scrollBox.viewport.height, 1);
277
- if (activeFlatIdx < scrollBox.scrollTop) {
278
- scrollBox.scrollTo(activeFlatIdx);
279
- } else if (activeFlatIdx >= scrollBox.scrollTop + viewportHeight) {
280
- scrollBox.scrollTo(activeFlatIdx - viewportHeight + 1);
281
- }
282
- }, [activeFlatIdx]);
283
-
284
235
  const columns = useMemo<WorldIndexColumn[]>(() => {
285
236
  const statusWidth = 1;
286
237
  const symbolWidth = 8;
@@ -350,31 +301,42 @@ export function WorldIndicesPane({ focused, width, height }: PaneProps) {
350
301
  }
351
302
  }, [quotes]);
352
303
 
304
+ const loadingCount = Array.from(quotes.values()).filter((state) => state.loading).length;
305
+ const latestQuoteTs = Math.max(
306
+ 0,
307
+ ...Array.from(quotes.values()).map((state) => state.quote?.lastUpdated ?? 0),
308
+ );
309
+ usePaneFooter("world-indices", () => ({
310
+ info: [
311
+ ...(loadingCount > 0 ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
312
+ ...(latestQuoteTs > 0 ? [{ id: "fresh", parts: [{ text: new Date(latestQuoteTs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), tone: "muted" as const }] }] : []),
313
+ ],
314
+ }), [latestQuoteTs, loadingCount]);
315
+
353
316
  return (
354
- <box flexDirection="column" width={width} height={height}>
355
- <DataTable<WorldIndexTableRow, WorldIndexColumn>
356
- columns={columns}
357
- items={flatRows}
358
- sortColumnId={sortPreference.columnId}
359
- sortDirection={sortPreference.direction}
360
- onHeaderClick={handleHeaderClick}
361
- headerScrollRef={headerScrollRef}
362
- scrollRef={scrollRef}
363
- syncHeaderScroll={syncHeaderScroll}
364
- onBodyScrollActivity={handleBodyScrollActivity}
365
- hoveredIdx={hoveredFlatIdx}
366
- setHoveredIdx={setHoveredFlatIdx}
367
- getItemKey={(row) => row.type === "header" ? `header-${row.region}` : row.entry.symbol}
368
- isSelected={(row) => row.type === "row" && row.entry.symbol === selectedSymbol}
369
- onSelect={(_row, index) => selectFlatIndex(index)}
370
- onActivate={(_row, index) => openSelected(index)}
371
- renderSectionHeader={(row) => row.type === "header"
372
- ? { text: REGION_LABELS[row.region] }
373
- : null}
374
- renderCell={renderCell}
375
- emptyStateTitle="No indices configured."
376
- />
377
- </box>
317
+ <DataTableView<WorldIndexTableRow, WorldIndexColumn>
318
+ focused={focused}
319
+ selectedIndex={activeFlatIdx}
320
+ isNavigable={(row) => row.type === "row"}
321
+ onSelectIndex={selectFlatIndex}
322
+ onActivateIndex={(index) => openSelected(index)}
323
+ rootWidth={width}
324
+ rootHeight={height}
325
+ columns={columns}
326
+ items={flatRows}
327
+ sortColumnId={sortPreference.columnId}
328
+ sortDirection={sortPreference.direction}
329
+ onHeaderClick={handleHeaderClick}
330
+ getItemKey={(row) => row.type === "header" ? `header-${row.region}` : row.entry.symbol}
331
+ isSelected={(row) => row.type === "row" && row.entry.symbol === selectedSymbol}
332
+ onSelect={(_row, index) => selectFlatIndex(index)}
333
+ onActivate={(_row, index) => openSelected(index)}
334
+ renderSectionHeader={(row) => row.type === "header"
335
+ ? { text: REGION_LABELS[row.region] }
336
+ : null}
337
+ renderCell={renderCell}
338
+ emptyStateTitle="No indices configured."
339
+ />
378
340
  );
379
341
  }
380
342
 
@@ -1,3 +1,4 @@
1
+
1
2
  export interface IndexEntry {
2
3
  symbol: string;
3
4
  name: string;
@@ -1,5 +1,8 @@
1
1
  import type { GloomPlugin } from "../../types/plugin";
2
+ import type { NewsArticle, NewsQuery } from "../../types/news-source";
3
+ import type { NewsItem } from "../../types/data-provider";
2
4
  import { YahooFinanceClient } from "../../sources/yahoo-finance";
5
+ import { assetDataProvider, newsProvider } from "../../capabilities";
3
6
 
4
7
  class YahooPluginProvider extends YahooFinanceClient {
5
8
  readonly priority = 1000;
@@ -9,10 +12,61 @@ export function createYahooProvider() {
9
12
  return new YahooPluginProvider();
10
13
  }
11
14
 
15
+ function yahooArticleId(item: NewsItem, ticker: string): string {
16
+ return item.url || `${ticker}:${item.title}:${item.publishedAt.toISOString()}`;
17
+ }
18
+
19
+ function mapYahooNewsItem(item: NewsItem, ticker: string): NewsArticle {
20
+ return {
21
+ id: yahooArticleId(item, ticker),
22
+ title: item.title,
23
+ url: item.url,
24
+ source: item.source || "Yahoo Finance",
25
+ publishedAt: item.publishedAt,
26
+ summary: item.summary,
27
+ topic: "ticker",
28
+ topics: ["ticker"],
29
+ sectors: [],
30
+ categories: [],
31
+ tickers: [ticker],
32
+ scores: {
33
+ importance: 0,
34
+ urgency: 0,
35
+ marketImpact: 0,
36
+ novelty: 0,
37
+ confidence: 0,
38
+ },
39
+ importance: 0,
40
+ isBreaking: false,
41
+ isDeveloping: false,
42
+ };
43
+ }
44
+
45
+ function createYahooNewsProvider(provider: YahooPluginProvider) {
46
+ return {
47
+ supports(query: NewsQuery): boolean {
48
+ const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
49
+ return feed === "ticker" && !!query.ticker;
50
+ },
51
+ async fetchNews(query: NewsQuery): Promise<NewsArticle[]> {
52
+ const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
53
+ if (feed !== "ticker" || !query.ticker) return [];
54
+ const ticker = query.ticker.trim().toUpperCase();
55
+ const items = await provider.getNews(ticker, query.limit ?? 50, query.exchange ?? "");
56
+ return items.map((item) => mapYahooNewsItem(item, ticker));
57
+ },
58
+ };
59
+ }
60
+
61
+ const yahooProvider = createYahooProvider();
62
+
12
63
  export const yahooPlugin: GloomPlugin = {
13
64
  id: "yahoo",
14
65
  name: "Yahoo Fallback",
15
66
  version: "1.0.0",
16
67
  description: "Built-in delayed fallback for quotes, fundamentals, charts, and unsupported cloud data.",
17
- dataProvider: createYahooProvider(),
68
+ capabilities: [
69
+ assetDataProvider(yahooProvider),
70
+ newsProvider({ id: "yahoo", name: "Yahoo Finance", priority: 1000, provider: createYahooNewsProvider(yahooProvider) }),
71
+ ],
18
72
  };