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
@@ -1,24 +1,32 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { Box, Text, Textarea } from "../../../ui";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch } from "react";
2
3
  import type { RefObject } from "react";
3
- import { TextAttributes, type ScrollBoxRenderable, type TextareaRenderable } from "@opentui/core";
4
- import { useKeyboard } from "@opentui/react";
4
+ import { TextAttributes, type TextareaRenderable } from "../../../ui";
5
+ import { useShortcut } from "../../../react/input";
5
6
  import type { PaneProps } from "../../../types/plugin";
6
7
  import type { ColumnConfig } from "../../../types/config";
8
+ import type { DataProvider } from "../../../types/data-provider";
7
9
  import type { InstrumentSearchResult } from "../../../types/instrument";
8
10
  import type { TickerFinancials } from "../../../types/financials";
9
11
  import type { TickerRecord } from "../../../types/ticker";
10
- import { useAppState, usePaneInstance, usePaneInstanceId } from "../../../state/app-context";
11
- import { usePluginPaneState, usePluginState } from "../../../plugins/plugin-runtime";
12
+ import {
13
+ useAppDispatch,
14
+ useAppSelector,
15
+ usePaneInstance,
16
+ usePaneInstanceId,
17
+ type AppAction,
18
+ } from "../../../state/app-context";
19
+ import { useAssetData, usePluginPaneState, usePluginState, usePluginTickerActions } from "../../../plugins/plugin-runtime";
12
20
  import { useFxRatesMap, useTickerFinancialsMap } from "../../../market-data/hooks";
13
21
  import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
14
22
  import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
15
23
  import { useQuoteStreaming } from "../../../state/use-quote-streaming";
16
- import { TickerListTable } from "../../../components/ticker-list-table";
17
- import { Button, EmptyState, Spinner } from "../../../components";
24
+ import { Button, EmptyState, SegmentedControl, Spinner, Tabs, TickerListTableView, usePaneFooter, type DataTableKeyEvent } from "../../../components";
18
25
  import { colors } from "../../../theme/colors";
26
+ import { canonicalExchange } from "../../../utils/exchanges";
19
27
  import { formatTimeAgo } from "../../../utils/format";
20
28
  import { getColumnValue, getSortValue, type ColumnContext } from "../portfolio-list/metrics";
21
- import { getSharedDataProvider, getSharedRegistry } from "../../registry";
29
+ import { getSharedRegistry } from "../../registry";
22
30
  import { upsertTickerFromSearchResult } from "../../../utils/ticker-search";
23
31
  import { buildGloomberbCliInstructions, resolveGloomberbCliCommand } from "./gloomberb-cli";
24
32
  import { detectProviders, getAiProvider, resolveDefaultAiProviderId } from "./providers";
@@ -125,14 +133,14 @@ function ScreenerPromptEditor({
125
133
  }, [editorKey, focused]);
126
134
 
127
135
  return (
128
- <box
136
+ <Box
129
137
  flexGrow={1}
130
138
  minHeight={10}
131
139
  border
132
140
  borderColor={colors.border}
133
141
  backgroundColor={colors.panel}
134
142
  >
135
- <textarea
143
+ <Textarea
136
144
  key={editorKey}
137
145
  ref={textareaRef}
138
146
  initialValue={initialValue}
@@ -144,7 +152,7 @@ function ScreenerPromptEditor({
144
152
  flexGrow={1}
145
153
  wrapText
146
154
  />
147
- </box>
155
+ </Box>
148
156
  );
149
157
  }
150
158
 
@@ -199,20 +207,20 @@ function resolveResultSymbol(result: InstrumentSearchResult): string {
199
207
 
200
208
  function matchesExchange(result: InstrumentSearchResult, exchange: string): boolean {
201
209
  if (!exchange) return true;
202
- const normalized = exchange.toUpperCase();
203
- return result.exchange.toUpperCase() === normalized
204
- || (result.primaryExchange?.toUpperCase() ?? "") === normalized
205
- || (result.brokerContract?.exchange?.toUpperCase() ?? "") === normalized
206
- || (result.brokerContract?.primaryExchange?.toUpperCase() ?? "") === normalized;
210
+ const normalized = canonicalExchange(exchange);
211
+ return canonicalExchange(result.exchange) === normalized
212
+ || canonicalExchange(result.primaryExchange) === normalized
213
+ || canonicalExchange(result.brokerContract?.exchange) === normalized
214
+ || canonicalExchange(result.brokerContract?.primaryExchange) === normalized;
207
215
  }
208
216
 
209
217
  async function resolveCandidateTicker(
210
218
  candidate: { symbol: string; exchange: string; reason: string },
211
219
  localTickers: ReadonlyMap<string, TickerRecord>,
212
- stateDispatch: ReturnType<typeof useAppState>["dispatch"],
220
+ stateDispatch: Dispatch<AppAction>,
221
+ dataProvider: DataProvider | null,
213
222
  ): Promise<ValidatedScreenerResult | null> {
214
223
  const registry = getSharedRegistry();
215
- const dataProvider = getSharedDataProvider() ?? registry?.dataProvider ?? null;
216
224
  if (!registry || !dataProvider) {
217
225
  throw new Error("AI screener could not access the ticker repository.");
218
226
  }
@@ -254,7 +262,8 @@ async function resolveCandidateTicker(
254
262
  async function validateScreenerResults(
255
263
  candidates: Array<{ symbol: string; exchange: string; reason: string }>,
256
264
  localTickers: ReadonlyMap<string, TickerRecord>,
257
- stateDispatch: ReturnType<typeof useAppState>["dispatch"],
265
+ stateDispatch: Dispatch<AppAction>,
266
+ dataProvider: DataProvider | null,
258
267
  ): Promise<{ results: ValidatedScreenerResult[]; warning: string | null }> {
259
268
  const resolved: ValidatedScreenerResult[] = [];
260
269
  const unresolved: string[] = [];
@@ -262,7 +271,7 @@ async function validateScreenerResults(
262
271
  const seen = new Set<string>();
263
272
 
264
273
  for (const candidate of candidates) {
265
- const result = await resolveCandidateTicker(candidate, localTickers, stateDispatch);
274
+ const result = await resolveCandidateTicker(candidate, localTickers, stateDispatch, dataProvider);
266
275
  if (!result) {
267
276
  unresolved.push(candidate.symbol);
268
277
  continue;
@@ -313,37 +322,15 @@ function sortScreenerRows(
313
322
  });
314
323
  }
315
324
 
316
- function ActionChip({
317
- label,
318
- onPress,
319
- active = false,
320
- disabled = false,
321
- }: {
322
- label: string;
323
- onPress?: () => void;
324
- active?: boolean;
325
- disabled?: boolean;
326
- }) {
327
- return (
328
- <box
329
- backgroundColor={active ? colors.selected : colors.panel}
330
- onMouseDown={!disabled && onPress ? (event: any) => {
331
- event.stopPropagation?.();
332
- event.preventDefault?.();
333
- onPress();
334
- } : undefined}
335
- >
336
- <text fg={disabled ? colors.textDim : active ? colors.selectedText : colors.text}>
337
- {` ${label} `}
338
- </text>
339
- </box>
340
- );
341
- }
342
-
343
325
  export function AiScreenerPane({ focused, width, height }: PaneProps) {
326
+ const dataProvider = useAssetData();
327
+ const { pinTicker } = usePluginTickerActions();
344
328
  const paneId = usePaneInstanceId();
345
329
  const paneInstance = usePaneInstance();
346
- const { state, dispatch } = useAppState();
330
+ const dispatch = useAppDispatch();
331
+ const tickers = useAppSelector((state) => state.tickers);
332
+ const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
333
+ const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
347
334
  const [providers] = useState(() => detectProviders());
348
335
  const availableProviders = providers.filter((provider) => provider.available);
349
336
  const selectableProviders = availableProviders.length > 0 ? availableProviders : providers;
@@ -355,15 +342,12 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
355
342
  const [activeTabId, setActiveTabId] = usePluginPaneState<string | null>("activeTabId", null);
356
343
  const [cursorSymbol, setCursorSymbol] = usePluginPaneState<string | null>("cursorSymbol", null);
357
344
  const [sorts, setSorts] = usePluginPaneState<Record<string, ScreenerSortPreference>>("sorts", {});
358
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
359
345
  const [runState, setRunState] = useState<RunState | null>(null);
360
346
  const [now, setNow] = useState(Date.now());
361
347
  const [editorState, setEditorState] = useState<ScreenerEditorState | null>(null);
362
348
  const [forceConfirmTabId, setForceConfirmTabId] = useState<string | null>(null);
363
349
  const runRef = useRef<AiRunController | null>(null);
364
350
  const editorTextareaRef = useRef<TextareaRenderable | null>(null);
365
- const headerScrollRef = useRef<ScrollBoxRenderable>(null);
366
- const scrollRef = useRef<ScrollBoxRenderable>(null);
367
351
  const initializedRef = useRef(false);
368
352
  const pendingInitialRunRef = useRef<string | null>(null);
369
353
  const lastTabClickRef = useRef<{ tabId: string; at: number } | null>(null);
@@ -421,25 +405,25 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
421
405
 
422
406
  const screenerTickers = useMemo(() => (
423
407
  (activeTab?.results ?? [])
424
- .map((result) => state.tickers.get(result.symbol) ?? null)
408
+ .map((result) => tickers.get(result.symbol) ?? null)
425
409
  .filter((ticker): ticker is TickerRecord => ticker != null)
426
- ), [activeTab?.results, state.tickers]);
410
+ ), [activeTab?.results, tickers]);
427
411
  const financialsMap = useTickerFinancialsMap(screenerTickers);
428
412
 
429
413
  const trackedCurrencies = useMemo(() => [
430
- state.config.baseCurrency,
414
+ baseCurrency,
431
415
  ...screenerTickers.map((ticker) => ticker.metadata.currency),
432
416
  ...screenerTickers.map((ticker) => financialsMap.get(ticker.metadata.ticker)?.quote?.currency ?? null),
433
- ], [financialsMap, screenerTickers, state.config.baseCurrency]);
417
+ ], [baseCurrency, financialsMap, screenerTickers]);
434
418
  const exchangeRates = useFxRatesMap(trackedCurrencies);
435
- const effectiveExchangeRates = exchangeRates.size > 1 || state.exchangeRates.size === 0
419
+ const effectiveExchangeRates = exchangeRates.size > 1 || cachedExchangeRates.size === 0
436
420
  ? exchangeRates
437
- : state.exchangeRates;
421
+ : cachedExchangeRates;
438
422
  const columnContext: ColumnContext = useMemo(() => ({
439
- baseCurrency: state.config.baseCurrency,
423
+ baseCurrency,
440
424
  exchangeRates: effectiveExchangeRates,
441
425
  now,
442
- }), [effectiveExchangeRates, now, state.config.baseCurrency]);
426
+ }), [baseCurrency, effectiveExchangeRates, now]);
443
427
 
444
428
  const sortedTickers = useMemo(
445
429
  () => sortScreenerRows(screenerTickers, resultMap, financialsMap, activeSort, columnContext, columns),
@@ -482,18 +466,6 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
482
466
  const isRunningActiveTab = runState?.tabId === activeTab?.id;
483
467
  const forceRunArmed = !!activeTab && forceConfirmTabId === activeTab.id;
484
468
 
485
- const syncHeaderScroll = useCallback(() => {
486
- const header = headerScrollRef.current;
487
- const body = scrollRef.current;
488
- if (header && body && header.scrollLeft !== body.scrollLeft) {
489
- header.scrollLeft = body.scrollLeft;
490
- }
491
- }, []);
492
-
493
- const handleBodyScrollActivity = useCallback(() => {
494
- syncHeaderScroll();
495
- }, [syncHeaderScroll]);
496
-
497
469
  const updateTabs = useCallback((updater: (tabs: AiScreenerTab[]) => AiScreenerTab[]) => {
498
470
  setPersistedState((current) => ({
499
471
  tabs: updater(normalizeTabs(current)),
@@ -668,7 +640,7 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
668
640
  rawOutput = await run.done;
669
641
 
670
642
  const parsed = parseScreenerResponse(rawOutput);
671
- const validated = await validateScreenerResults(parsed.tickers, state.tickers, dispatch);
643
+ const validated = await validateScreenerResults(parsed.tickers, tickers, dispatch, dataProvider);
672
644
  const nextResults = !samePrompt || mode === "force"
673
645
  ? validated.results
674
646
  : mergeScreenerResults(tab.results, validated.results);
@@ -697,7 +669,7 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
697
669
  }
698
670
  setRunState((current) => (current?.tabId === tab.id ? null : current));
699
671
  }
700
- }, [dispatch, providers, setForceConfirmTabId, state.tickers, tabs, upsertTab]);
672
+ }, [dataProvider, dispatch, providers, setForceConfirmTabId, tabs, tickers, upsertTab]);
701
673
 
702
674
  useEffect(() => {
703
675
  if (!pendingInitialRunRef.current) return;
@@ -721,30 +693,6 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
721
693
  }
722
694
  }, [activeTab, forceConfirmTabId]);
723
695
 
724
- useEffect(() => {
725
- if (headerScrollRef.current) {
726
- headerScrollRef.current.horizontalScrollBar.visible = false;
727
- }
728
- syncHeaderScroll();
729
- }, [columns, syncHeaderScroll]);
730
-
731
- useEffect(() => {
732
- const scrollBox = scrollRef.current;
733
- if (!scrollBox) return;
734
- const viewportHeight = scrollBox.viewport.height;
735
- if (safeSelectedIdx < scrollBox.scrollTop) {
736
- scrollBox.scrollTo(safeSelectedIdx);
737
- } else if (safeSelectedIdx >= scrollBox.scrollTop + viewportHeight) {
738
- scrollBox.scrollTo(Math.max(0, safeSelectedIdx - viewportHeight + 1));
739
- }
740
- }, [safeSelectedIdx]);
741
-
742
- useEffect(() => {
743
- const scrollBox = scrollRef.current;
744
- if (!scrollBox) return;
745
- scrollBox.verticalScrollBar.visible = sortedTickers.length > scrollBox.viewport.height;
746
- }, [sortedTickers.length, width, height]);
747
-
748
696
  const cycleTabs = useCallback((direction: -1 | 1) => {
749
697
  if (!activeTab || tabs.length <= 1) return;
750
698
  const currentIndex = tabs.findIndex((tab) => tab.id === activeTab.id);
@@ -768,7 +716,19 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
768
716
  });
769
717
  }, [activeTab, setSorts, sorts]);
770
718
 
771
- useKeyboard((event) => {
719
+ const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
720
+ const isEnter = event.name === "enter" || event.name === "return";
721
+ if (!isEnter || !cursorSymbol) return false;
722
+ event.preventDefault?.();
723
+ event.stopPropagation?.();
724
+ pinTicker(cursorSymbol, {
725
+ floating: !!event.shift,
726
+ paneType: "ticker-detail",
727
+ });
728
+ return true;
729
+ }, [cursorSymbol, pinTicker]);
730
+
731
+ useShortcut((event) => {
772
732
  if (!focused) return;
773
733
 
774
734
  if (editorState) {
@@ -794,22 +754,6 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
794
754
  }
795
755
 
796
756
  const key = event.name;
797
- const isEnter = key === "enter" || key === "return";
798
-
799
- if (key === "j" || key === "down") {
800
- const nextTicker = sortedTickers[Math.min(safeSelectedIdx + 1, sortedTickers.length - 1)];
801
- if (nextTicker) {
802
- setCursorSymbol(nextTicker.metadata.ticker);
803
- }
804
- return;
805
- }
806
- if (key === "k" || key === "up") {
807
- const nextTicker = sortedTickers[Math.max(safeSelectedIdx - 1, 0)];
808
- if (nextTicker) {
809
- setCursorSymbol(nextTicker.metadata.ticker);
810
- }
811
- return;
812
- }
813
757
  if (event.name === "t") {
814
758
  addTab();
815
759
  return;
@@ -840,15 +784,7 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
840
784
  }
841
785
  if (event.name === "e") {
842
786
  editActiveTab();
843
- return;
844
787
  }
845
-
846
- if (!isEnter || !cursorSymbol) return;
847
- const registry = getSharedRegistry();
848
- registry?.pinTickerFn(cursorSymbol, {
849
- floating: !!event.shift,
850
- paneType: "ticker-detail",
851
- });
852
788
  });
853
789
 
854
790
  useEffect(() => () => {
@@ -858,7 +794,7 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
858
794
  const contentHeight = Math.max(height - 8, 4);
859
795
  const warningColor = colors.borderFocused;
860
796
  const statusText = activeTab
861
- ? isRunningActiveTab
797
+ ? isRunningActiveTab && runState
862
798
  ? `${runState.mode === "force" ? "Force refreshing" : "Refreshing"} with ${getAiProvider(activeTab.providerId, providers)?.name ?? "AI"}...`
863
799
  : activeTab.lastSuccessAt
864
800
  ? `Last ran ${formatTimeAgo(new Date(activeTab.lastSuccessAt))}`
@@ -888,97 +824,130 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
888
824
  ]
889
825
  : activeTab
890
826
  ? ["No validated results yet."]
891
- : ["Press t to create an AI screener tab."];
827
+ : ["Create an AI screener tab to begin."];
892
828
  const paddedDetailLines = [...detailLines];
893
829
  while (paddedDetailLines.length < DETAIL_FOOTER_LINES) {
894
830
  paddedDetailLines.push("");
895
831
  }
832
+ const activeProvider = activeTab ? getAiProvider(activeTab.providerId, providers) : null;
833
+ const refreshActiveTab = useCallback(() => {
834
+ if (!activeTab || isRunningActiveTab) return;
835
+ void runTab(activeTab.id, "refresh");
836
+ }, [activeTab, isRunningActiveTab, runTab]);
837
+ const forceRefreshActiveTab = useCallback(() => {
838
+ if (!activeTab || isRunningActiveTab) return;
839
+ if (forceRunArmed) {
840
+ setForceConfirmTabId(null);
841
+ void runTab(activeTab.id, "force");
842
+ return;
843
+ }
844
+ setForceConfirmTabId(activeTab.id);
845
+ }, [activeTab, forceRunArmed, isRunningActiveTab, runTab, setForceConfirmTabId]);
846
+
847
+ usePaneFooter("ai-screener", () => ({
848
+ info: [
849
+ ...(editorState ? [{
850
+ id: "provider",
851
+ parts: [{ text: editorProvider?.name ?? editorState.providerId, tone: editorProvider?.available === false ? "warning" as const : "value" as const, bold: true }],
852
+ }] : activeTab ? [{
853
+ id: "provider",
854
+ parts: [{ text: activeProvider?.name ?? activeTab.providerId, tone: activeProvider?.available === false ? "warning" as const : "value" as const, bold: true }],
855
+ }] : []),
856
+ { id: "status", parts: [{ text: statusText, tone: isRunningActiveTab ? "muted" : "value" }] },
857
+ ...(forceRunArmed ? [{ id: "force", parts: [{ text: "force refresh armed", tone: "warning" as const }] }] : []),
858
+ ],
859
+ hints: editorState
860
+ ? [
861
+ { id: "save", key: "Ctrl+S", label: "save", onPress: saveEditor },
862
+ { id: "provider", key: "Ctrl+P", label: "provider", onPress: () => cycleEditorProvider(1), disabled: selectableProviders.length <= 1 },
863
+ ]
864
+ : [
865
+ { id: "new", key: "t", label: "new", onPress: addTab },
866
+ { id: "close", key: "w", label: "close tab", onPress: activeTab ? () => removeTab(activeTab.id) : undefined, disabled: !activeTab },
867
+ { id: "refresh", key: "r", label: "efresh", onPress: refreshActiveTab, disabled: !activeTab || isRunningActiveTab },
868
+ { id: "force-refresh", key: "Shift+R", label: "force refresh", onPress: forceRefreshActiveTab, disabled: !activeTab || isRunningActiveTab },
869
+ { id: "edit", key: "e", label: "dit", onPress: editActiveTab, disabled: !activeTab },
870
+ ],
871
+ }), [
872
+ activeProvider?.available,
873
+ activeProvider?.name,
874
+ activeTab,
875
+ addTab,
876
+ cycleEditorProvider,
877
+ editActiveTab,
878
+ editorProvider?.available,
879
+ editorProvider?.name,
880
+ editorState,
881
+ forceRefreshActiveTab,
882
+ forceRunArmed,
883
+ isRunningActiveTab,
884
+ refreshActiveTab,
885
+ removeTab,
886
+ saveEditor,
887
+ selectableProviders.length,
888
+ statusText,
889
+ ]);
896
890
 
897
891
  return (
898
- <box flexDirection="column" width={width} height={height}>
899
- <box flexDirection="row" height={1}>
900
- {displayTabs.map((tab) => {
901
- const isDraft = tab.draft === true;
902
- const isActive = isDraft ? editorState?.mode === "create" : tab.id === activeTab?.id;
903
- return (
904
- <box key={tab.id} flexDirection="row">
905
- <box
906
- onMouseDown={() => {
907
- if (editorState) return;
908
- if (isDraft) return;
909
- setActiveTabId(tab.id);
910
- setCursorSymbol(null);
911
- const now = Date.now();
912
- const last = lastTabClickRef.current;
913
- if (last?.tabId === tab.id && now - last.at <= 350) {
914
- lastTabClickRef.current = null;
915
- editTab(tab);
916
- return;
917
- }
918
- lastTabClickRef.current = { tabId: tab.id, at: now };
919
- }}
920
- >
921
- <text
922
- fg={isActive ? colors.textBright : colors.textDim}
923
- bg={isActive ? colors.selected : undefined}
924
- attributes={isActive ? TextAttributes.BOLD : 0}
925
- >
926
- {` ${truncateWithEllipsis(tab.title, isDraft ? 20 : 18)} `}
927
- </text>
928
- </box>
929
- {isActive && !isDraft ? (
930
- <text fg={colors.textMuted} onMouseDown={() => { if (!editorState) removeTab(tab.id); }}>{`x `}</text>
931
- ) : (
932
- <text>{` `}</text>
933
- )}
934
- </box>
935
- );
936
- })}
937
- <text fg={colors.textMuted} onMouseDown={() => { if (!editorState) addTab(); }}>{` + `}</text>
938
- <box flexGrow={1} />
939
- <text fg={colors.textMuted}>
940
- {editorState
941
- ? editorState.mode === "create" ? "creating new screener" : "editing prompt"
942
- : activeTab
943
- ? `${activeTab.results.length} tickers`
944
- : "t new"}
945
- </text>
946
- </box>
947
-
948
- <box flexDirection="row" height={1} gap={1}>
892
+ <Box flexDirection="column" width={width} height={height}>
893
+ <Box height={1}>
894
+ <Tabs
895
+ tabs={displayTabs.map((tab) => {
896
+ const isDraft = tab.draft === true;
897
+ return {
898
+ label: truncateWithEllipsis(tab.title, isDraft ? 20 : 18),
899
+ value: tab.id,
900
+ onClose: !editorState && !isDraft ? removeTab : undefined,
901
+ };
902
+ })}
903
+ activeValue={editorState?.mode === "create" ? "__draft__" : activeTab?.id ?? null}
904
+ onSelect={(tabId) => {
905
+ if (editorState || tabId === "__draft__") return;
906
+ const tab = tabs.find((entry) => entry.id === tabId);
907
+ if (!tab) return;
908
+ setActiveTabId(tab.id);
909
+ setCursorSymbol(null);
910
+ const now = Date.now();
911
+ const last = lastTabClickRef.current;
912
+ if (last?.tabId === tab.id && now - last.at <= 350) {
913
+ lastTabClickRef.current = null;
914
+ editTab(tab);
915
+ return;
916
+ }
917
+ lastTabClickRef.current = { tabId: tab.id, at: now };
918
+ }}
919
+ compact
920
+ variant="pill"
921
+ closeMode="active"
922
+ onAdd={editorState ? undefined : addTab}
923
+ />
924
+ </Box>
925
+
926
+ <Box flexDirection="row" height={1} gap={1}>
949
927
  {editorState ? (
950
928
  <>
951
- <Button label="Save" variant="primary" shortcut="Ctrl+S" onPress={saveEditor} />
952
- <Button label="Cancel" variant="ghost" shortcut="Esc" onPress={closeEditor} />
929
+ <Button label="Save" variant="primary" onPress={saveEditor} />
930
+ <Button label="Cancel" variant="ghost" onPress={closeEditor} />
953
931
  </>
954
932
  ) : (
955
933
  <>
956
934
  {isRunningActiveTab ? (
957
935
  <>
958
936
  <Button label={runState?.mode === "force" ? "Force Refreshing..." : "Refreshing..."} variant="secondary" disabled />
959
- <Button label="Stop" variant="ghost" shortcut="Esc" onPress={cancelRun} />
937
+ <Button label="Stop" variant="ghost" onPress={cancelRun} />
960
938
  </>
961
939
  ) : (
962
940
  <>
963
941
  <Button
964
942
  label={primaryRunLabel}
965
943
  variant="primary"
966
- shortcut="r"
967
- onPress={activeTab ? () => { void runTab(activeTab.id, "refresh"); } : undefined}
944
+ onPress={refreshActiveTab}
968
945
  disabled={!activeTab}
969
946
  />
970
947
  <Button
971
948
  label={forceRunArmed ? "Confirm Force Refresh" : "Force Refresh"}
972
949
  variant={forceRunArmed ? "danger" : "ghost"}
973
- shortcut="Shift+R"
974
- onPress={activeTab ? () => {
975
- if (forceRunArmed) {
976
- setForceConfirmTabId(null);
977
- void runTab(activeTab.id, "force");
978
- return;
979
- }
980
- setForceConfirmTabId(activeTab.id);
981
- } : undefined}
950
+ onPress={forceRefreshActiveTab}
982
951
  disabled={!activeTab}
983
952
  />
984
953
  </>
@@ -986,128 +955,116 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
986
955
  <Button
987
956
  label="Edit Prompt"
988
957
  variant={promptDirty ? "primary" : "secondary"}
989
- shortcut="e"
990
958
  onPress={editActiveTab}
991
959
  disabled={!activeTab}
992
960
  />
993
961
  </>
994
962
  )}
995
- <box flexGrow={1} />
996
- <text fg={colors.textMuted}>
997
- {editorState
998
- ? `${editorProvider?.name ?? editorState.providerId} · Ctrl+S save · Esc cancel`
999
- : activeTab
1000
- ? `${getAiProvider(activeTab.providerId, providers)?.name ?? activeTab.providerId} · ${forceRunArmed ? "Force refresh armed. Click again to confirm." : statusText}`
1001
- : statusText}
1002
- </text>
1003
- </box>
963
+ </Box>
1004
964
 
1005
965
  {availableProviders.length === 0 && (
1006
- <box flexDirection="column" paddingX={1} paddingTop={1}>
1007
- <text fg={colors.textDim}>No AI CLI tools detected. Install `claude`, `gemini`, or `codex` to run screeners.</text>
1008
- </box>
966
+ <Box flexDirection="column" paddingX={1} paddingTop={1}>
967
+ <Text fg={colors.textDim}>No AI CLI tools detected. Install `claude`, `gemini`, or `codex` to run screeners.</Text>
968
+ </Box>
1009
969
  )}
1010
970
 
1011
971
  {editorState ? (
1012
972
  <>
1013
- <box flexDirection="column" paddingX={1} paddingTop={1} gap={1}>
1014
- <text fg={colors.textDim}>
973
+ <Box flexDirection="column" paddingX={1} paddingTop={1} gap={1}>
974
+ <Text fg={colors.textDim}>
1015
975
  {editorState.mode === "create"
1016
976
  ? "Describe the companies or setups you want this screener to discover."
1017
977
  : "Update the screener prompt or provider. Saving does not rerun it automatically."}
1018
- </text>
1019
- <box flexDirection="row" gap={1} flexWrap="wrap">
1020
- {selectableProviders.map((provider) => (
1021
- <ActionChip
1022
- key={provider.id}
1023
- label={provider.available ? provider.name : `${provider.name} (missing)`}
1024
- onPress={() => {
1025
- setEditorState((current) => current
1026
- ? { ...current, providerId: provider.id, error: null }
1027
- : current);
1028
- }}
1029
- active={editorState.providerId === provider.id}
1030
- />
1031
- ))}
1032
- </box>
978
+ </Text>
979
+ <SegmentedControl
980
+ value={editorState.providerId}
981
+ options={selectableProviders.map((provider) => ({
982
+ value: provider.id,
983
+ label: provider.available ? provider.name : `${provider.name} (missing)`,
984
+ }))}
985
+ onChange={(providerId) => {
986
+ setEditorState((current) => current
987
+ ? { ...current, providerId, error: null }
988
+ : current);
989
+ }}
990
+ />
1033
991
  {editorState.error ? (
1034
- <text fg={colors.negative}>{editorState.error}</text>
992
+ <Text fg={colors.negative}>{editorState.error}</Text>
1035
993
  ) : (
1036
- <text fg={colors.textDim}>
1037
- The AI will return validated ticker ideas with a short reason for each one. Ctrl+P cycles providers.
1038
- </text>
994
+ <Text fg={colors.textDim}>
995
+ The AI will return validated ticker ideas with a short reason for each one.
996
+ </Text>
1039
997
  )}
1040
- </box>
998
+ </Box>
1041
999
 
1042
- <box flexGrow={1} minHeight={contentHeight} padding={1}>
1000
+ <Box flexGrow={1} minHeight={contentHeight} padding={1}>
1043
1001
  <ScreenerPromptEditor
1044
1002
  editorKey={editorState.key}
1045
1003
  initialValue={editorState.prompt}
1046
1004
  focused={focused}
1047
1005
  textareaRef={editorTextareaRef}
1048
1006
  />
1049
- </box>
1007
+ </Box>
1050
1008
 
1051
- <box height={1} paddingX={1}>
1052
- <text fg={colors.textDim}>{"\u2500".repeat(Math.max(width - 2, 0))}</text>
1053
- </box>
1009
+ <Box height={1} paddingX={1}>
1010
+ <Text fg={colors.textDim}>{"\u2500".repeat(Math.max(width - 2, 0))}</Text>
1011
+ </Box>
1054
1012
 
1055
- <box flexDirection="column" paddingX={1}>
1056
- <text fg={colors.textDim}>
1013
+ <Box flexDirection="column" paddingX={1}>
1014
+ <Text fg={colors.textDim}>
1057
1015
  {editorProvider?.available === false
1058
1016
  ? `${editorProvider.name} is not currently installed. Save and switch later.`
1059
- : "Click a provider chip or press Ctrl+P to switch. Save to keep the draft."}
1060
- </text>
1061
- </box>
1017
+ : "Click a provider chip to switch. Save to keep the draft."}
1018
+ </Text>
1019
+ </Box>
1062
1020
  </>
1063
1021
  ) : (
1064
1022
  <>
1065
1023
  {activeTab?.lastError && (
1066
- <box flexDirection="column" paddingX={1} paddingTop={1}>
1024
+ <Box flexDirection="column" paddingX={1} paddingTop={1}>
1067
1025
  {wrapTextLines(activeTab.lastError, detailTextWidth, 2).map((line, index) => (
1068
- <box key={`error:${index}`} height={1}>
1069
- <text fg={colors.negative}>{line || " "}</text>
1070
- </box>
1026
+ <Box key={`error:${index}`} height={1}>
1027
+ <Text fg={colors.negative}>{line || " "}</Text>
1028
+ </Box>
1071
1029
  ))}
1072
- </box>
1030
+ </Box>
1073
1031
  )}
1074
1032
 
1075
1033
  {activeTab?.lastWarning && !activeTab.lastError && (
1076
- <box flexDirection="column" paddingX={1} paddingTop={1}>
1034
+ <Box flexDirection="column" paddingX={1} paddingTop={1}>
1077
1035
  {wrapTextLines(activeTab.lastWarning, detailTextWidth, 2).map((line, index) => (
1078
- <box key={`warning:${index}`} height={1}>
1079
- <text fg={warningColor}>{line || " "}</text>
1080
- </box>
1036
+ <Box key={`warning:${index}`} height={1}>
1037
+ <Text fg={warningColor}>{line || " "}</Text>
1038
+ </Box>
1081
1039
  ))}
1082
- </box>
1040
+ </Box>
1083
1041
  )}
1084
1042
 
1085
- {summaryLines.length > 0 && !activeTab.lastError && (
1086
- <box flexDirection="column" paddingX={1} paddingTop={activeTab.lastWarning ? 0 : 1}>
1043
+ {activeTab && summaryLines.length > 0 && !activeTab.lastError && (
1044
+ <Box flexDirection="column" paddingX={1} paddingTop={activeTab.lastWarning ? 0 : 1}>
1087
1045
  {summaryLines.map((line, index) => (
1088
- <box key={`summary:${index}`} height={1}>
1089
- <text fg={colors.textDim}>{line || " "}</text>
1090
- </box>
1046
+ <Box key={`summary:${index}`} height={1}>
1047
+ <Text fg={colors.textDim}>{line || " "}</Text>
1048
+ </Box>
1091
1049
  ))}
1092
- </box>
1050
+ </Box>
1093
1051
  )}
1094
1052
 
1095
- <box flexGrow={1} minHeight={contentHeight}>
1053
+ <Box flexGrow={1} minHeight={contentHeight}>
1096
1054
  {!activeTab ? (
1097
- <box padding={1} flexGrow={1}>
1098
- <EmptyState title="No AI screeners yet." hint="Press t or click + to create one." />
1099
- </box>
1055
+ <Box padding={1} flexGrow={1}>
1056
+ <EmptyState title="No AI screeners yet." hint="Click + to create one." />
1057
+ </Box>
1100
1058
  ) : isRunningActiveTab && activeTab.results.length === 0 ? (
1101
- <box padding={1} flexGrow={1}>
1059
+ <Box padding={1} flexGrow={1}>
1102
1060
  <Spinner label="Running AI screener..." />
1103
- </box>
1061
+ </Box>
1104
1062
  ) : (
1105
- <TickerListTable
1063
+ <TickerListTableView
1064
+ focused={focused}
1106
1065
  columns={columns}
1107
1066
  tickers={sortedTickers}
1108
1067
  cursorSymbol={cursorSymbol}
1109
- hoveredIdx={hoveredIdx}
1110
- setHoveredIdx={setHoveredIdx}
1111
1068
  setCursorSymbol={setCursorSymbol}
1112
1069
  resolveCell={(column, ticker, financials) => {
1113
1070
  if (column.id === "ticker") {
@@ -1123,40 +1080,38 @@ export function AiScreenerPane({ focused, width, height }: PaneProps) {
1123
1080
  return getColumnValue(column, ticker, financials, columnContext);
1124
1081
  }}
1125
1082
  financialsMap={financialsMap}
1126
- headerScrollRef={headerScrollRef}
1127
- scrollRef={scrollRef}
1128
- syncHeaderScroll={syncHeaderScroll}
1129
- onBodyScrollActivity={handleBodyScrollActivity}
1130
1083
  sortColumnId={activeSort.columnId}
1131
1084
  sortDirection={activeSort.direction}
1132
1085
  onHeaderClick={handleHeaderClick}
1086
+ onRootKeyDown={handleTableKeyDown}
1087
+ resetScrollKey={activeTab.id}
1133
1088
  onRowActivate={(ticker) => {
1134
- getSharedRegistry()?.pinTickerFn(ticker.metadata.ticker, { floating: true, paneType: "ticker-detail" });
1089
+ pinTicker(ticker.metadata.ticker, { floating: true, paneType: "ticker-detail" });
1135
1090
  }}
1136
1091
  emptyTitle="No matches yet."
1137
- emptyHint={promptDirty ? "Prompt changed. Refresh to rerun." : "Press r to run this screener. Use PS to customize columns."}
1092
+ emptyHint={promptDirty ? "Prompt changed. Refresh to rerun." : "Run this screener. Use PS to customize columns."}
1138
1093
  />
1139
1094
  )}
1140
- </box>
1095
+ </Box>
1141
1096
 
1142
- <box height={1} paddingX={1}>
1143
- <text fg={colors.textDim}>{"\u2500".repeat(Math.max(width - 2, 0))}</text>
1144
- </box>
1097
+ <Box height={1} paddingX={1}>
1098
+ <Text fg={colors.textDim}>{"\u2500".repeat(Math.max(width - 2, 0))}</Text>
1099
+ </Box>
1145
1100
 
1146
- <box flexDirection="column" paddingX={1} minHeight={DETAIL_FOOTER_LINES}>
1101
+ <Box flexDirection="column" paddingX={1} minHeight={DETAIL_FOOTER_LINES}>
1147
1102
  {paddedDetailLines.map((line, index) => (
1148
- <box key={`detail:${index}`} height={1}>
1149
- <text
1103
+ <Box key={`detail:${index}`} height={1}>
1104
+ <Text
1150
1105
  fg={promptDirty ? warningColor : index === 0 && selectedResult ? colors.text : colors.textDim}
1151
1106
  attributes={index === 0 && selectedResult ? TextAttributes.BOLD : 0}
1152
1107
  >
1153
1108
  {line || " "}
1154
- </text>
1155
- </box>
1109
+ </Text>
1110
+ </Box>
1156
1111
  ))}
1157
- </box>
1112
+ </Box>
1158
1113
  </>
1159
1114
  )}
1160
- </box>
1115
+ </Box>
1161
1116
  );
1162
1117
  }