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,24 +1,76 @@
1
- import { useEffect, useState } from "react";
2
- import { useKeyboard } from "@opentui/react";
3
- import { TextAttributes } from "@opentui/core";
4
- import type { GloomPlugin, DetailTabProps } from "../../types/plugin";
1
+ import { Box, Text } from "../../ui";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { TextAttributes } from "../../ui";
4
+ import type { DetailTabProps, GloomPlugin, PaneProps } from "../../types/plugin";
5
5
  import type { OptionContract, OptionsChain } from "../../types/financials";
6
6
  import { usePaneTicker } from "../../state/app-context";
7
- import { ListView, type ListViewItem } from "../../components/ui";
8
- import { colors, hoverBg } from "../../theme/colors";
9
- import { padTo, formatCompact, formatNumber } from "../../utils/format";
7
+ import { blendHex, colors } from "../../theme/colors";
8
+ import { formatCompact, formatNumber } from "../../utils/format";
10
9
  import { formatMarketPrice } from "../../utils/market-format";
11
10
  import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
12
11
  import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks";
13
- import { Spinner } from "../../components/spinner";
14
- import { setOptionsAvailability } from "./options-availability";
12
+ import { DataTableView, Spinner, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../components";
13
+ import { createTickerSurfacePaneTemplate } from "./ticker-surface";
14
+
15
+ type OptionColumnId =
16
+ | "callLast"
17
+ | "callBid"
18
+ | "callAsk"
19
+ | "callVolume"
20
+ | "callOpenInterest"
21
+ | "callIv"
22
+ | "strike"
23
+ | "putLast"
24
+ | "putBid"
25
+ | "putAsk"
26
+ | "putVolume"
27
+ | "putOpenInterest"
28
+ | "putIv";
29
+
30
+ type OptionColumn = DataTableColumn & { id: OptionColumnId };
31
+
32
+ interface OptionTableRow {
33
+ strike: number;
34
+ call?: OptionContract;
35
+ put?: OptionContract;
36
+ isPositionStrike: boolean;
37
+ }
38
+
39
+ type OptionsViewProps = Pick<DetailTabProps, "width" | "height" | "focused"> & {
40
+ onCapture?: DetailTabProps["onCapture"];
41
+ };
15
42
 
16
- function OptionsTab({ width, height, focused, onCapture }: DetailTabProps) {
17
- const { ticker } = usePaneTicker();
43
+ const OPTION_CALL_COLOR = "#5ed69a";
44
+ const OPTION_PUT_COLOR = "#ff9c7a";
45
+ const OPTION_PRICE_COLOR = "#dfc05b";
46
+ const OPTION_ACTIVITY_COLOR = "#35a7d6";
47
+ const OPTION_IV_COLOR = "#8bd878";
48
+ const OPTION_STRIKE_COLOR = "#8fb7ff";
49
+
50
+ const OPTION_COLUMNS: OptionColumn[] = [
51
+ { id: "callOpenInterest", label: "C OI", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
52
+ { id: "callVolume", label: "C VOL", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
53
+ { id: "callLast", label: "C LAST", width: 7, align: "right", headerColor: OPTION_CALL_COLOR },
54
+ { id: "callIv", label: "C IV", width: 6, align: "right", headerColor: OPTION_IV_COLOR },
55
+ { id: "callBid", label: "C BID", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
56
+ { id: "callAsk", label: "C ASK", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
57
+ { id: "strike", label: "STRIKE", width: 9, align: "right", headerColor: OPTION_STRIKE_COLOR },
58
+ { id: "putBid", label: "P BID", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
59
+ { id: "putAsk", label: "P ASK", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
60
+ { id: "putIv", label: "P IV", width: 6, align: "right", headerColor: OPTION_IV_COLOR },
61
+ { id: "putLast", label: "P LAST", width: 7, align: "right", headerColor: OPTION_PUT_COLOR },
62
+ { id: "putVolume", label: "P VOL", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
63
+ { id: "putOpenInterest", label: "P OI", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
64
+ ];
65
+
66
+ export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
67
+ const { ticker, financials } = usePaneTicker();
18
68
  const [expIdx, setExpIdx] = useState(0);
19
69
  const [strikeIdx, setStrikeIdx] = useState(0);
70
+ const [autoScrollVersion, setAutoScrollVersion] = useState(0);
71
+ const [scrollToIndexAlign, setScrollToIndexAlign] = useState<"nearest" | "center">("nearest");
20
72
  const [interactive, setInteractive] = useState(false);
21
- const [hoveredExpIdx, setHoveredExpIdx] = useState<number | null>(null);
73
+ const userSelectedStrikeRef = useRef(false);
22
74
  const target = resolveOptionsTarget(ticker);
23
75
  const isOpt = target?.isOptionTicker ?? false;
24
76
  const parsed = target?.parsedOption ?? null;
@@ -70,20 +122,13 @@ function OptionsTab({ width, height, focused, onCapture }: DetailTabProps) {
70
122
 
71
123
  // Exit interactive mode when ticker changes
72
124
  useEffect(() => {
125
+ userSelectedStrikeRef.current = false;
126
+ setScrollToIndexAlign("nearest");
73
127
  exitInteractive();
74
128
  setExpIdx(0);
75
129
  setStrikeIdx(0);
76
130
  }, [effectiveTicker]);
77
131
 
78
- useEffect(() => {
79
- setHoveredExpIdx(null);
80
- }, [expIdx]);
81
-
82
- useEffect(() => {
83
- if (!target) return;
84
- setOptionsAvailability(target, !!chain && chain.expirationDates.length > 0);
85
- }, [chain, target]);
86
-
87
132
  useEffect(() => {
88
133
  if (!parsed || !initialChain || initialChain.expirationDates.length === 0) return;
89
134
  const bestExpIdx = initialChain.expirationDates.reduce((best, ts, i) =>
@@ -94,64 +139,123 @@ function OptionsTab({ width, height, focused, onCapture }: DetailTabProps) {
94
139
  }, [expIdx, initialChain, parsed]);
95
140
 
96
141
  useEffect(() => {
97
- setStrikeIdx(0);
142
+ userSelectedStrikeRef.current = false;
98
143
  }, [expIdx]);
99
144
 
100
145
  // Build sorted strike list from the union of calls and puts
101
- const strikes = chain ? buildStrikeList(chain) : [];
146
+ const strikes = useMemo(() => chain ? buildStrikeList(chain) : [], [chain]);
147
+ const callsByStrike = useMemo(
148
+ () => new Map<number, OptionContract>(chain?.calls.map((c) => [c.strike, c]) ?? []),
149
+ [chain],
150
+ );
151
+ const putsByStrike = useMemo(
152
+ () => new Map<number, OptionContract>(chain?.puts.map((p) => [p.strike, p]) ?? []),
153
+ [chain],
154
+ );
155
+ const rows = useMemo<OptionTableRow[]>(() => strikes.map((strike) => ({
156
+ strike,
157
+ call: callsByStrike.get(strike),
158
+ put: putsByStrike.get(strike),
159
+ isPositionStrike: !!parsed && Math.abs(strike - parsed.strike) < 0.01,
160
+ })), [callsByStrike, parsed, putsByStrike, strikes]);
161
+ const selectedStrike = strikes[strikeIdx];
162
+ const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
163
+ const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
102
164
 
103
- // Auto-scroll to matching strike when viewing an option position
104
- useEffect(() => {
105
- if (!parsed || strikes.length === 0) return;
106
- const matchIdx = strikes.findIndex((s) => Math.abs(s - parsed.strike) < 0.01);
107
- if (matchIdx >= 0) setStrikeIdx(matchIdx);
108
- }, [strikes.length, parsed?.strike]);
165
+ usePaneFooter("options", () => {
166
+ const info = [
167
+ ...(selectedExpiration != null ? [{ id: "exp", parts: [{ text: formatExpDate(selectedExpiration), tone: "muted" as const }] }] : []),
168
+ ...(selectedStrike != null ? [{ id: "strike", parts: [{ text: `Strike ${formatStrikeLabel(selectedStrike)}`, tone: "value" as const, bold: true }] }] : []),
169
+ ...(selectedCall ? [{ id: "call-iv", parts: [{ text: "Call IV", tone: "label" as const }, { text: formatIv(selectedCall.impliedVolatility), tone: "value" as const }] }] : []),
170
+ ...(selectedPut ? [{ id: "put-iv", parts: [{ text: "Put IV", tone: "label" as const }, { text: formatIv(selectedPut.impliedVolatility), tone: "value" as const }] }] : []),
171
+ ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
172
+ ...(error ? [{ id: "error", parts: [{ text: "error", tone: "warning" as const }] }] : []),
173
+ ];
174
+ return info.length > 0 ? { info } : null;
175
+ }, [
176
+ error,
177
+ loading,
178
+ selectedCall?.impliedVolatility,
179
+ selectedExpiration,
180
+ selectedPut?.impliedVolatility,
181
+ selectedStrike,
182
+ ]);
109
183
 
110
- // Keyboard navigation
111
- useKeyboard((event) => {
112
- if (!focused || !chain) return;
184
+ useEffect(() => {
185
+ setStrikeIdx((index) => {
186
+ if (strikes.length === 0) return 0;
187
+ return Math.min(index, strikes.length - 1);
188
+ });
189
+ }, [strikes.length]);
113
190
 
191
+ useEffect(() => {
192
+ if (strikes.length === 0 || userSelectedStrikeRef.current) return;
193
+ const targetStrike = resolveDefaultStrikeTarget(parsed?.strike, financials?.quote?.price);
194
+ if (targetStrike == null) return;
195
+ setScrollToIndexAlign("center");
196
+ setStrikeIdx(findNearestStrikeIndex(strikes, targetStrike));
197
+ setAutoScrollVersion((version) => version + 1);
198
+ }, [expIdx, financials?.quote?.price, parsed?.strike, strikes]);
199
+
200
+ const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
114
201
  const isEnter = event.name === "enter" || event.name === "return";
115
202
 
116
- // Enter/Escape for interactive mode
117
203
  if (isEnter && !interactive) {
204
+ event.preventDefault?.();
205
+ event.stopPropagation?.();
118
206
  enterInteractive();
119
- return;
207
+ return true;
120
208
  }
121
209
  if (event.name === "escape" && interactive) {
210
+ event.preventDefault?.();
211
+ event.stopPropagation?.();
122
212
  exitInteractive();
123
- return;
213
+ return true;
124
214
  }
125
215
 
126
- if (!interactive) return;
127
-
128
- // Arrow keys + j/k/h/l in interactive mode
129
- const numExp = chain.expirationDates.length;
130
216
  if (event.name === "j" || event.name === "down") {
217
+ if (strikes.length === 0) return true;
218
+ event.preventDefault?.();
219
+ event.stopPropagation?.();
220
+ userSelectedStrikeRef.current = true;
221
+ setScrollToIndexAlign("nearest");
131
222
  setStrikeIdx((i) => Math.min(i + 1, strikes.length - 1));
132
- } else if (event.name === "k" || event.name === "up") {
223
+ return true;
224
+ }
225
+ if (event.name === "k" || event.name === "up") {
226
+ if (strikes.length === 0) return true;
227
+ event.preventDefault?.();
228
+ event.stopPropagation?.();
229
+ userSelectedStrikeRef.current = true;
230
+ setScrollToIndexAlign("nearest");
133
231
  setStrikeIdx((i) => Math.max(i - 1, 0));
134
- } else if (event.name === "h" || event.name === "left") {
232
+ return true;
233
+ }
234
+
235
+ if (!interactive) return false;
236
+
237
+ const numExp = chain?.expirationDates.length ?? 0;
238
+ if (event.name === "h" || event.name === "left") {
239
+ event.preventDefault?.();
240
+ event.stopPropagation?.();
135
241
  setExpIdx((i) => Math.max(i - 1, 0));
136
- } else if (event.name === "l" || event.name === "right") {
137
- setExpIdx((i) => Math.min(i + 1, numExp - 1));
242
+ return true;
243
+ }
244
+ if (event.name === "l" || event.name === "right") {
245
+ event.preventDefault?.();
246
+ event.stopPropagation?.();
247
+ setExpIdx((i) => Math.min(i + 1, Math.max(numExp - 1, 0)));
248
+ return true;
138
249
  }
139
- });
250
+ return false;
251
+ }, [chain?.expirationDates.length, enterInteractive, exitInteractive, interactive, strikes.length]);
140
252
 
141
- if (!ticker) return <text fg={colors.textDim}>Select a ticker to view options.</text>;
253
+ if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view options.</Text>;
142
254
  if (loading && !chain) return <Spinner label="Loading options chain..." />;
143
- if (error) return <text fg={colors.textDim}>{error}</text>;
144
- if (!chain || chain.expirationDates.length === 0) return <text fg={colors.textDim}>No options available for {effectiveTicker}.</text>;
255
+ if (error) return <Text fg={colors.textDim}>{error}</Text>;
256
+ if (!chain || chain.expirationDates.length === 0) return <Text fg={colors.textDim}>No options available for {effectiveTicker}.</Text>;
145
257
 
146
258
  const innerWidth = Math.max(width - 4, 60);
147
- const callsByStrike = new Map<number, OptionContract>(chain.calls.map((c) => [c.strike, c]));
148
- const putsByStrike = new Map<number, OptionContract>(chain.puts.map((p) => [p.strike, p]));
149
-
150
- // Column widths for each side
151
- const colW = { last: 7, bid: 7, ask: 7, vol: 6, oi: 6 };
152
- const sideW = colW.last + colW.bid + colW.ask + colW.vol + colW.oi + 4; // 4 gaps
153
- const strikeW = 9;
154
- const divW = 1;
155
259
 
156
260
  // Position info
157
261
  const posShares = isOpt && parsed
@@ -163,157 +267,74 @@ function OptionsTab({ width, height, focused, onCapture }: DetailTabProps) {
163
267
  const expStart = Math.max(0, Math.min(expIdx - Math.floor(maxExpVisible / 2), chain.expirationDates.length - maxExpVisible));
164
268
  const visibleExps = chain.expirationDates.slice(expStart, expStart + maxExpVisible);
165
269
 
166
- // Selected row detail
167
- const selectedStrike = strikes[strikeIdx];
168
- const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
169
- const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
170
- const strikeItems: ListViewItem[] = strikes.map((strike) => ({
171
- id: String(strike),
172
- label: formatStrikeLabel(strike),
173
- }));
174
-
175
- const hoverColor = hoverBg();
176
-
177
270
  return (
178
- <box flexDirection="column" flexGrow={1} paddingX={1} onMouseDown={() => { if (!interactive) enterInteractive(); }}>
271
+ <Box flexDirection="column" flexGrow={1} paddingX={1} onMouseDown={() => { if (!interactive) enterInteractive(); }}>
179
272
  {/* Expiration selector */}
180
- <box flexDirection="row" height={1} gap={1}>
181
- <text fg={colors.textDim}>Exp:</text>
182
- {visibleExps.map((ts, i) => {
183
- const realIdx = expStart + i;
184
- const isActive = realIdx === expIdx;
185
- const isHovered = realIdx === hoveredExpIdx && !isActive;
186
- return (
187
- <box
188
- key={ts}
189
- onMouseMove={() => setHoveredExpIdx(realIdx)}
190
- onMouseOut={() => setHoveredExpIdx(null)}
191
- onMouseDown={() => { enterInteractive(); setExpIdx(realIdx); }}
192
- >
193
- <text
194
- fg={isActive ? colors.textBright : isHovered ? colors.text : colors.textMuted}
195
- attributes={isActive ? TextAttributes.BOLD : isHovered ? TextAttributes.UNDERLINE : 0}
196
- >
197
- {isActive ? `[${formatExpDate(ts)}]` : ` ${formatExpDate(ts)} `}
198
- </text>
199
- </box>
200
- );
201
- })}
202
- {loading && <spinner name="dots" color={colors.textDim} />}
203
- </box>
273
+ <Box flexDirection="row" height={1} gap={1}>
274
+ <Text fg={colors.textDim}>Exp:</Text>
275
+ <Box flexGrow={1} height={1}>
276
+ <Tabs
277
+ tabs={visibleExps.map((ts, i) => ({
278
+ label: formatExpDate(ts),
279
+ value: String(expStart + i),
280
+ }))}
281
+ activeValue={String(expIdx)}
282
+ onSelect={(value) => {
283
+ enterInteractive();
284
+ setExpIdx(Number(value));
285
+ }}
286
+ compact
287
+ variant="bare"
288
+ />
289
+ </Box>
290
+ {loading && <Spinner />}
291
+ </Box>
204
292
 
205
293
  {/* Position banner */}
206
294
  {isOpt && parsed && (
207
- <box height={1}>
208
- <text fg={colors.textBright}>
295
+ <Box height={1}>
296
+ <Text fg={colors.textBright}>
209
297
  {`Position: ${posShares} ${parsed.side === "C" ? "call" : "put"} contract${posShares !== 1 ? "s" : ""} @ $${parsed.strike}`}
210
- </text>
211
- </box>
298
+ </Text>
299
+ </Box>
212
300
  )}
213
301
 
214
- {/* Header labels */}
215
- <box flexDirection="row" height={1}>
216
- <box width={sideW}><text attributes={TextAttributes.BOLD} fg={colors.positive}>CALLS</text></box>
217
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
218
- <box width={strikeW} />
219
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
220
- <box width={sideW}><text attributes={TextAttributes.BOLD} fg={colors.negative}>PUTS</text></box>
221
- </box>
222
-
223
- {/* Column headers */}
224
- <box flexDirection="row" height={1}>
225
- <box width={sideW}>
226
- <text attributes={TextAttributes.BOLD} fg={colors.textDim}>
227
- {padTo("Last", colW.last)} {padTo("Bid", colW.bid)} {padTo("Ask", colW.ask)} {padTo("Vol", colW.vol)} {padTo("OI", colW.oi)}
228
- </text>
229
- </box>
230
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
231
- <box width={strikeW}>
232
- <text attributes={TextAttributes.BOLD} fg={colors.textDim}>{padTo("Strike", strikeW, "center")}</text>
233
- </box>
234
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
235
- <box width={sideW}>
236
- <text attributes={TextAttributes.BOLD} fg={colors.textDim}>
237
- {padTo("Last", colW.last)} {padTo("Bid", colW.bid)} {padTo("Ask", colW.ask)} {padTo("Vol", colW.vol)} {padTo("OI", colW.oi)}
238
- </text>
239
- </box>
240
- </box>
241
-
242
- {/* Chain rows */}
243
- <ListView
244
- items={strikeItems}
245
- selectedIndex={interactive ? strikeIdx : -1}
246
- scrollIndex={strikeIdx}
247
- onSelect={(index) => {
302
+ <DataTableView<OptionTableRow, OptionColumn>
303
+ focused={focused}
304
+ selectedIndex={strikeIdx}
305
+ onRootKeyDown={handleTableKeyDown}
306
+ columns={OPTION_COLUMNS}
307
+ items={rows}
308
+ sortColumnId={null}
309
+ sortDirection="asc"
310
+ onHeaderClick={() => {}}
311
+ getItemKey={(row) => String(row.strike)}
312
+ isSelected={(_row, index) => index === strikeIdx}
313
+ onSelect={(_row, index) => {
314
+ userSelectedStrikeRef.current = true;
315
+ setScrollToIndexAlign("nearest");
248
316
  enterInteractive();
249
317
  setStrikeIdx(index);
250
318
  }}
251
- renderRow={(_, state, i) => {
252
- const strike = strikes[i]!;
253
- const call = callsByStrike.get(strike);
254
- const put = putsByStrike.get(strike);
255
- const isSelected = state.selected;
256
- const isPositionStrike = parsed && Math.abs(strike - parsed.strike) < 0.01;
257
- const callItm = call?.inTheMoney;
258
- const putItm = put?.inTheMoney;
259
-
260
- return (
261
- <box flexDirection="row">
262
- <box width={sideW}>
263
- <text fg={callItm ? colors.textBright : colors.text}>
264
- {formatContractRow(call, colW)}
265
- </text>
266
- </box>
267
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
268
- <box width={strikeW}>
269
- <text fg={isSelected ? colors.textBright : colors.neutral} attributes={isSelected ? TextAttributes.BOLD : 0}>
270
- {padTo(formatStrikeLabel(strike), strikeW, "center")}
271
- </text>
272
- </box>
273
- <box width={divW}><text fg={colors.textDim}>{"\u2502"}</text></box>
274
- <box width={sideW}>
275
- <text fg={putItm ? colors.textBright : colors.text}>
276
- {formatContractRow(put, colW)}
277
- </text>
278
- </box>
279
- </box>
280
- );
281
- }}
282
- getRowBackgroundColor={(_, state, i) => {
283
- const strike = strikes[i]!;
284
- const isPositionStrike = parsed && Math.abs(strike - parsed.strike) < 0.01;
285
- if (state.selected) return colors.selected;
286
- if (state.hovered) return hoverColor;
287
- return isPositionStrike ? colors.selected : colors.bg;
288
- }}
289
- hoverBgColor={hoverColor}
290
- flexGrow={1}
291
- scrollable
319
+ renderCell={renderOptionCell}
320
+ emptyStateTitle="No strikes available."
321
+ scrollToIndex={strikeIdx}
322
+ scrollToIndexAlign={scrollToIndexAlign}
323
+ scrollToIndexVersion={autoScrollVersion}
292
324
  />
293
325
 
294
- {/* Detail for selected row */}
295
- {interactive && (selectedCall || selectedPut) && (
296
- <box height={1}>
297
- <text fg={colors.textDim}>
298
- {selectedCall ? `Call IV: ${(selectedCall.impliedVolatility * 100).toFixed(1)}%` : ""}
299
- {selectedCall && selectedPut ? " | " : ""}
300
- {selectedPut ? `Put IV: ${(selectedPut.impliedVolatility * 100).toFixed(1)}%` : ""}
301
- </text>
302
- </box>
303
- )}
304
-
305
- {/* Help */}
306
- <box height={1}>
307
- <text fg={colors.textMuted}>
308
- {interactive
309
- ? "j/k/\u2191\u2193 strike h/l/\u2190\u2192 expiration Esc exit"
310
- : "Enter to interact"}
311
- </text>
312
- </box>
313
- </box>
326
+ </Box>
314
327
  );
315
328
  }
316
329
 
330
+ export function OptionsTab(props: DetailTabProps) {
331
+ return <OptionsView {...props} />;
332
+ }
333
+
334
+ export function OptionsPane({ focused, width, height }: PaneProps) {
335
+ return <OptionsView focused={focused} width={width} height={height} />;
336
+ }
337
+
317
338
  function buildStrikeList(chain: OptionsChain): number[] {
318
339
  const set = new Set<number>();
319
340
  for (const c of chain.calls) set.add(c.strike);
@@ -321,30 +342,125 @@ function buildStrikeList(chain: OptionsChain): number[] {
321
342
  return Array.from(set).sort((a, b) => a - b);
322
343
  }
323
344
 
345
+ function resolveDefaultStrikeTarget(
346
+ optionStrike: number | undefined,
347
+ quotePrice: number | undefined,
348
+ ): number | null {
349
+ if (optionStrike != null && Number.isFinite(optionStrike)) return optionStrike;
350
+ if (quotePrice != null && Number.isFinite(quotePrice)) return quotePrice;
351
+ return null;
352
+ }
353
+
354
+ function findNearestStrikeIndex(strikes: number[], targetStrike: number): number {
355
+ let bestIndex = 0;
356
+ let bestDistance = Number.POSITIVE_INFINITY;
357
+ for (let index = 0; index < strikes.length; index += 1) {
358
+ const distance = Math.abs(strikes[index]! - targetStrike);
359
+ if (distance < bestDistance) {
360
+ bestIndex = index;
361
+ bestDistance = distance;
362
+ }
363
+ }
364
+ return bestIndex;
365
+ }
366
+
324
367
  function formatStrikeLabel(strike: number): string {
325
368
  const decimals = strike % 1 === 0 ? 0 : 2;
326
369
  return formatNumber(strike, decimals).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
327
370
  }
328
371
 
329
- interface ColWidths {
330
- last: number;
331
- bid: number;
332
- ask: number;
333
- vol: number;
334
- oi: number;
372
+ function formatIv(value: number | undefined): string {
373
+ if (value == null || !Number.isFinite(value)) return "—";
374
+ return `${(value * 100).toFixed(1)}%`;
375
+ }
376
+
377
+ function optionContractForColumn(row: OptionTableRow, columnId: OptionColumnId): OptionContract | undefined {
378
+ return columnId.startsWith("call") ? row.call : row.put;
335
379
  }
336
380
 
337
- function formatContractRow(contract: { lastPrice: number; bid: number; ask: number; volume: number; openInterest: number } | undefined, w: ColWidths): string {
338
- if (!contract) {
339
- return padTo("—", w.last) + " " + padTo("—", w.bid) + " " + padTo("—", w.ask) + " " + padTo("—", w.vol) + " " + padTo("—", w.oi);
381
+ function optionColumnColor(columnId: OptionColumnId): string {
382
+ if (columnId === "strike") return OPTION_STRIKE_COLOR;
383
+ if (columnId.endsWith("Iv")) return OPTION_IV_COLOR;
384
+ if (columnId.endsWith("Volume") || columnId.endsWith("OpenInterest")) return OPTION_ACTIVITY_COLOR;
385
+ if (columnId.endsWith("Bid") || columnId.endsWith("Ask")) return OPTION_PRICE_COLOR;
386
+ return columnId.startsWith("call") ? OPTION_CALL_COLOR : OPTION_PUT_COLOR;
387
+ }
388
+
389
+ function optionMoneynessBackground(
390
+ row: OptionTableRow,
391
+ contract: OptionContract | undefined,
392
+ columnId: OptionColumnId,
393
+ rowState: { selected: boolean; hovered: boolean },
394
+ ): string | undefined {
395
+ if (rowState.selected || rowState.hovered) return undefined;
396
+ const inTheMoney = inferColumnMoneyness(row, contract, columnId);
397
+ const sideColor = columnId.startsWith("call") ? colors.positive : colors.negative;
398
+ return inTheMoney
399
+ ? blendHex(colors.bg, sideColor, 0.13)
400
+ : blendHex(colors.bg, colors.neutral, 0.055);
401
+ }
402
+
403
+ function inferColumnMoneyness(
404
+ row: OptionTableRow,
405
+ contract: OptionContract | undefined,
406
+ columnId: OptionColumnId,
407
+ ): boolean {
408
+ if (contract) return contract.inTheMoney;
409
+ const oppositeContract = columnId.startsWith("call") ? row.put : row.call;
410
+ return oppositeContract ? !oppositeContract.inTheMoney : false;
411
+ }
412
+
413
+ function formatOptionContractCell(contract: OptionContract | undefined, column: OptionColumn): string {
414
+ if (!contract) return "—";
415
+ switch (column.id) {
416
+ case "callLast":
417
+ case "putLast":
418
+ return formatMarketPrice(contract.lastPrice, { assetCategory: "OPT", maxWidth: column.width });
419
+ case "callBid":
420
+ case "putBid":
421
+ return formatMarketPrice(contract.bid, { assetCategory: "OPT", maxWidth: column.width });
422
+ case "callAsk":
423
+ case "putAsk":
424
+ return formatMarketPrice(contract.ask, { assetCategory: "OPT", maxWidth: column.width });
425
+ case "callVolume":
426
+ case "putVolume":
427
+ return formatCompact(contract.volume);
428
+ case "callOpenInterest":
429
+ case "putOpenInterest":
430
+ return formatCompact(contract.openInterest);
431
+ case "callIv":
432
+ case "putIv":
433
+ return formatIv(contract.impliedVolatility);
434
+ case "strike":
435
+ return formatStrikeLabel(contract.strike);
340
436
  }
341
- return (
342
- padTo(formatMarketPrice(contract.lastPrice, { assetCategory: "OPT", maxWidth: w.last }), w.last) + " " +
343
- padTo(formatMarketPrice(contract.bid, { assetCategory: "OPT", maxWidth: w.bid }), w.bid) + " " +
344
- padTo(formatMarketPrice(contract.ask, { assetCategory: "OPT", maxWidth: w.ask }), w.ask) + " " +
345
- padTo(formatCompact(contract.volume), w.vol) + " " +
346
- padTo(formatCompact(contract.openInterest), w.oi)
347
- );
437
+ }
438
+
439
+ function renderOptionCell(
440
+ row: OptionTableRow,
441
+ column: OptionColumn,
442
+ _index: number,
443
+ rowState: { selected: boolean; hovered: boolean },
444
+ ): DataTableCell {
445
+ const selectedColor = rowState.selected ? colors.selectedText : undefined;
446
+
447
+ if (column.id === "strike") {
448
+ return {
449
+ text: formatStrikeLabel(row.strike),
450
+ color: selectedColor ?? OPTION_STRIKE_COLOR,
451
+ backgroundColor: rowState.selected || rowState.hovered
452
+ ? undefined
453
+ : blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1),
454
+ attributes: rowState.selected || row.isPositionStrike ? TextAttributes.BOLD : TextAttributes.NONE,
455
+ };
456
+ }
457
+
458
+ const contract = optionContractForColumn(row, column.id);
459
+ return {
460
+ text: formatOptionContractCell(contract, column),
461
+ color: selectedColor ?? (contract ? optionColumnColor(column.id) : colors.textDim),
462
+ backgroundColor: optionMoneynessBackground(row, contract, column.id, rowState),
463
+ };
348
464
  }
349
465
 
350
466
  export const optionsPlugin: GloomPlugin = {
@@ -354,12 +470,36 @@ export const optionsPlugin: GloomPlugin = {
354
470
  description: "View options chain for tickers",
355
471
  toggleable: true,
356
472
 
473
+ panes: [
474
+ {
475
+ id: "options",
476
+ name: "Options",
477
+ icon: "O",
478
+ component: OptionsPane,
479
+ defaultPosition: "right",
480
+ defaultMode: "floating",
481
+ defaultFloatingSize: { width: 112, height: 28 },
482
+ },
483
+ ],
484
+
485
+ paneTemplates: [
486
+ createTickerSurfacePaneTemplate({
487
+ id: "options-pane",
488
+ paneId: "options",
489
+ label: "Options",
490
+ description: "Options chain for the selected ticker.",
491
+ keywords: ["options", "chain", "calls", "puts", "omon"],
492
+ shortcut: "OMON",
493
+ }),
494
+ ],
495
+
357
496
  setup(ctx) {
358
497
  ctx.registerDetailTab({
359
498
  id: "options",
360
499
  name: "Options",
361
500
  order: 35, // after Chart (30), before News (40)
362
501
  component: OptionsTab,
502
+ isVisible: ({ hasOptionsChain }) => hasOptionsChain,
363
503
  });
364
504
  },
365
505
  };