gloomberb 0.5.1 → 0.6.1

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 (506) hide show
  1. package/README.md +91 -11
  2. package/package.json +13 -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 +318 -158
  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 +53 -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/native/surface-visibility.ts +48 -0
  53. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  54. package/src/components/chart/static-chart-surface.tsx +191 -0
  55. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  56. package/src/components/chart/stock-chart.tsx +452 -191
  57. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  58. package/src/components/command-bar/command-bar.test.tsx +269 -20
  59. package/src/components/command-bar/command-bar.tsx +830 -353
  60. package/src/components/command-bar/command-registry.ts +10 -20
  61. package/src/components/command-bar/helpers.test.ts +1 -1
  62. package/src/components/command-bar/helpers.ts +9 -11
  63. package/src/components/command-bar/pane-template-display.ts +4 -8
  64. package/src/components/command-bar/root-shortcuts.ts +12 -11
  65. package/src/components/command-bar/view-model.test.ts +8 -42
  66. package/src/components/command-bar/view-model.ts +8 -36
  67. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  68. package/src/components/command-bar/workflow-ops.ts +40 -9
  69. package/src/components/command-bar/workflow-types.ts +2 -1
  70. package/src/components/data-table-stack-view.test.tsx +206 -0
  71. package/src/components/data-table-stack-view.tsx +152 -0
  72. package/src/components/data-table-view.test.tsx +188 -0
  73. package/src/components/data-table-view.tsx +279 -0
  74. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  75. package/src/components/feed-data-table-stack-view.tsx +365 -0
  76. package/src/components/index.ts +26 -31
  77. package/src/components/layout/detached-pane-shell.tsx +185 -0
  78. package/src/components/layout/floating-pane.tsx +87 -14
  79. package/src/components/layout/header.tsx +139 -49
  80. package/src/components/layout/pane-content.tsx +42 -0
  81. package/src/components/layout/pane-footer.test.tsx +195 -0
  82. package/src/components/layout/pane-footer.tsx +425 -0
  83. package/src/components/layout/pane-header.test.tsx +39 -0
  84. package/src/components/layout/pane-header.tsx +192 -14
  85. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  86. package/src/components/layout/pane-sizing.ts +9 -10
  87. package/src/components/layout/pane-title.ts +35 -0
  88. package/src/components/layout/pane.tsx +52 -11
  89. package/src/components/layout/shell.test.tsx +315 -107
  90. package/src/components/layout/shell.tsx +1010 -368
  91. package/src/components/layout/status-bar.test.tsx +17 -10
  92. package/src/components/layout/status-bar.tsx +367 -100
  93. package/src/components/layout/titlebar-overlay.ts +2 -0
  94. package/src/components/markdown-editor.tsx +29 -14
  95. package/src/components/markdown-text.tsx +25 -24
  96. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  97. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  98. package/src/components/pane-settings-dialog.tsx +521 -89
  99. package/src/components/pane-template-wizard.test.tsx +1 -1
  100. package/src/components/pane-template-wizard.tsx +38 -37
  101. package/src/components/price-selector-dialog.tsx +22 -21
  102. package/src/components/ticker-badge-list.tsx +53 -0
  103. package/src/components/ticker-badge-text.test.tsx +44 -1
  104. package/src/components/ticker-badge-text.tsx +78 -38
  105. package/src/components/ticker-badge.tsx +28 -7
  106. package/src/components/ticker-list-table-view.tsx +248 -0
  107. package/src/components/ticker-list-table.test.tsx +47 -1
  108. package/src/components/ticker-list-table.tsx +212 -41
  109. package/src/components/toggle-list.tsx +114 -8
  110. package/src/components/ui/button.tsx +24 -16
  111. package/src/components/ui/choice-dialog.tsx +89 -0
  112. package/src/components/ui/data-table.tsx +313 -114
  113. package/src/components/ui/external-link.tsx +52 -13
  114. package/src/components/ui/fields.tsx +44 -30
  115. package/src/components/ui/frame.tsx +24 -57
  116. package/src/components/ui/index.ts +15 -50
  117. package/src/components/ui/list-view.tsx +65 -25
  118. package/src/components/ui/loading.tsx +5 -60
  119. package/src/components/ui/message-composer.test.tsx +81 -0
  120. package/src/components/ui/message-composer.tsx +139 -0
  121. package/src/components/ui/multi-select-dialog.tsx +100 -32
  122. package/src/components/ui/multi-select.ts +112 -0
  123. package/src/components/ui/native-select.tsx +120 -0
  124. package/src/components/ui/page-stack-view.tsx +49 -19
  125. package/src/components/ui/remote-image.tsx +50 -0
  126. package/src/components/ui/row-value-cache.test.ts +17 -0
  127. package/src/components/ui/row-value-cache.ts +41 -0
  128. package/src/components/ui/shortcut-hint.tsx +55 -0
  129. package/src/components/ui/status.tsx +12 -67
  130. package/src/components/ui/table-layout.ts +59 -0
  131. package/src/components/ui/tabs.tsx +249 -27
  132. package/src/components/ui/toggle.tsx +19 -127
  133. package/src/components/ui/ui.test.tsx +266 -53
  134. package/src/components/use-double-click-activation.ts +7 -2
  135. package/src/components/use-external-link-footer.ts +79 -0
  136. package/src/core/app-services.ts +95 -0
  137. package/src/core/state/app-state.ts +886 -0
  138. package/src/{state → core/state}/session-persistence.ts +7 -6
  139. package/src/data/config-layout.ts +225 -0
  140. package/src/data/config-store-node.ts +390 -0
  141. package/src/data/config-store.test.ts +72 -0
  142. package/src/data/config-store.ts +35 -478
  143. package/src/data/sqlite-json.ts +2 -5
  144. package/src/data/sqlite-retry.ts +1 -0
  145. package/src/index.tsx +2 -68
  146. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  147. package/src/market-data/coordinator.test.ts +73 -4
  148. package/src/market-data/coordinator.ts +190 -24
  149. package/src/market-data/hooks.test.tsx +1 -1
  150. package/src/market-data/hooks.tsx +109 -87
  151. package/src/market-data/query-store.ts +3 -3
  152. package/src/market-data/request-types.test.ts +0 -14
  153. package/src/market-data/result-types.ts +1 -0
  154. package/src/news/aggregator.test.ts +267 -0
  155. package/src/news/aggregator.ts +452 -0
  156. package/src/news/hooks.ts +39 -0
  157. package/src/news/ticker-symbols.ts +21 -0
  158. package/src/news/types.ts +77 -0
  159. package/src/notifications/app-notifier.ts +21 -10
  160. package/src/pane-settings.test.ts +0 -2
  161. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  162. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  163. package/src/plugins/builtin/ai/providers.ts +1 -9
  164. package/src/plugins/builtin/ai/runner.ts +41 -5
  165. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  166. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  167. package/src/plugins/builtin/ai/settings.ts +0 -1
  168. package/src/plugins/builtin/ai/utils.ts +1 -71
  169. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  170. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  171. package/src/plugins/builtin/alerts/index.tsx +263 -155
  172. package/src/plugins/builtin/alerts/types.ts +7 -0
  173. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  174. package/src/plugins/builtin/analytics/index.tsx +78 -120
  175. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  176. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  177. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  178. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  179. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  180. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  181. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  182. package/src/plugins/builtin/chat-controller.ts +85 -14
  183. package/src/plugins/builtin/chat.test.tsx +283 -36
  184. package/src/plugins/builtin/chat.tsx +363 -170
  185. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  186. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  187. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  188. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  189. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  190. package/src/plugins/builtin/correlation/index.tsx +51 -46
  191. package/src/plugins/builtin/correlation/settings.ts +1 -2
  192. package/src/plugins/builtin/debug.tsx +99 -70
  193. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  194. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  195. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  196. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  197. package/src/plugins/builtin/earnings/index.tsx +187 -94
  198. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  199. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  200. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  201. package/src/plugins/builtin/econ/index.tsx +236 -421
  202. package/src/plugins/builtin/econ/types.ts +1 -0
  203. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  204. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  205. package/src/plugins/builtin/help.test.tsx +120 -60
  206. package/src/plugins/builtin/help.tsx +266 -82
  207. package/src/plugins/builtin/holders/index.test.ts +45 -0
  208. package/src/plugins/builtin/holders/index.tsx +949 -0
  209. package/src/plugins/builtin/insider/index.tsx +207 -253
  210. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  211. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  212. package/src/plugins/builtin/layout-manager.tsx +12 -40
  213. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  214. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  215. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  216. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  217. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  218. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  219. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  220. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  221. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  222. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  223. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  224. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  225. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  226. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  227. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  228. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  229. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  230. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  231. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  232. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  233. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  234. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  235. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  236. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  237. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  238. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  239. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  240. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  241. package/src/plugins/builtin/news.tsx +98 -25
  242. package/src/plugins/builtin/notes-files.ts +51 -10
  243. package/src/plugins/builtin/notes.tsx +332 -131
  244. package/src/plugins/builtin/options.test.tsx +185 -0
  245. package/src/plugins/builtin/options.tsx +346 -206
  246. package/src/plugins/builtin/plugin-groups.ts +92 -0
  247. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  248. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  249. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  250. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  251. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  252. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  253. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  254. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  255. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  256. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  257. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  258. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  259. package/src/plugins/builtin/research/index.tsx +724 -0
  260. package/src/plugins/builtin/sec.tsx +67 -19
  261. package/src/plugins/builtin/sectors/index.tsx +42 -80
  262. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  263. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  264. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  265. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  266. package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
  267. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  268. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  269. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
  270. package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
  271. package/src/plugins/builtin/ticker-surface.ts +68 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  273. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  274. package/src/plugins/builtin/yahoo.ts +55 -1
  275. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  276. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  277. package/src/plugins/catalog-ui.test.ts +24 -0
  278. package/src/plugins/catalog-ui.ts +44 -0
  279. package/src/plugins/catalog.ts +2 -50
  280. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  281. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  282. package/src/plugins/ibkr/account-cache.ts +15 -120
  283. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  284. package/src/plugins/ibkr/config.test.ts +0 -18
  285. package/src/plugins/ibkr/dialogs.tsx +14 -56
  286. package/src/plugins/ibkr/flex.test.ts +57 -2
  287. package/src/plugins/ibkr/flex.ts +116 -12
  288. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  289. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  290. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  291. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  292. package/src/plugins/ibkr/gateway-types.ts +27 -0
  293. package/src/plugins/ibkr/index.test.tsx +3 -11
  294. package/src/plugins/ibkr/index.tsx +1 -0
  295. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  296. package/src/plugins/ibkr/price-normalization.ts +1 -0
  297. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  298. package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
  299. package/src/plugins/ibkr/trade-tab.tsx +166 -117
  300. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  301. package/src/plugins/pane-manager.test.ts +3 -1
  302. package/src/plugins/pane-manager.ts +93 -19
  303. package/src/plugins/plugin-runtime.test.tsx +176 -74
  304. package/src/plugins/plugin-runtime.tsx +228 -10
  305. package/src/plugins/prediction-markets/cache.ts +49 -0
  306. package/src/plugins/prediction-markets/categories.ts +0 -10
  307. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  308. package/src/plugins/prediction-markets/chart.tsx +21 -20
  309. package/src/plugins/prediction-markets/cli.ts +1 -0
  310. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  311. package/src/plugins/prediction-markets/controller.ts +37 -108
  312. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  313. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  314. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  315. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  316. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  317. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  318. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  319. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  320. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  321. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  322. package/src/plugins/prediction-markets/launch.ts +1 -1
  323. package/src/plugins/prediction-markets/metrics.ts +25 -12
  324. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  325. package/src/plugins/prediction-markets/pane.tsx +180 -82
  326. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  327. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  328. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  329. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  330. package/src/plugins/prediction-markets/settings.ts +0 -5
  331. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  332. package/src/plugins/registry.test.ts +307 -0
  333. package/src/plugins/registry.ts +293 -82
  334. package/src/react/input.ts +61 -0
  335. package/src/react/plugins/plugin-slot.tsx +21 -0
  336. package/src/react/use-raf-callback.ts +44 -0
  337. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  338. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  339. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  340. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  341. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  342. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  343. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  344. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  345. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  346. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  347. package/src/renderers/electrobun/bun/index.ts +1149 -0
  348. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  349. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  350. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  351. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  352. package/src/renderers/electrobun/view/app-services.ts +348 -0
  353. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  354. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  355. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  356. package/src/renderers/electrobun/view/config-host.ts +35 -0
  357. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  358. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  359. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  360. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  361. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  362. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  363. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  364. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  365. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  366. package/src/renderers/electrobun/view/input-host.tsx +112 -0
  367. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  368. package/src/renderers/electrobun/view/key-event.ts +134 -0
  369. package/src/renderers/electrobun/view/main.tsx +104 -0
  370. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  372. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  373. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  374. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  375. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  376. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  377. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  378. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  379. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  380. package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
  381. package/src/renderers/electrobun/view/update-host.ts +38 -0
  382. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  383. package/src/renderers/opentui/dialog-host.tsx +56 -0
  384. package/src/renderers/opentui/host.tsx +140 -0
  385. package/src/renderers/opentui/image-loader.test.ts +25 -0
  386. package/src/renderers/opentui/image-loader.ts +105 -0
  387. package/src/renderers/opentui/image-surface.tsx +266 -0
  388. package/src/renderers/opentui/input-host.tsx +63 -0
  389. package/src/renderers/opentui/start.tsx +71 -0
  390. package/src/renderers/opentui/test-utils.tsx +76 -0
  391. package/src/renderers/opentui/toast-host.tsx +10 -0
  392. package/src/renderers/opentui/ui-host.tsx +98 -0
  393. package/src/sources/gloomberb-cloud.test.ts +288 -17
  394. package/src/sources/gloomberb-cloud.ts +176 -9
  395. package/src/sources/provider-errors.ts +1 -0
  396. package/src/sources/provider-router.test.ts +220 -81
  397. package/src/sources/provider-router.ts +294 -111
  398. package/src/sources/sec-edgar.ts +19 -6
  399. package/src/sources/yahoo-finance.test.ts +0 -5
  400. package/src/sources/yahoo-finance.ts +420 -11
  401. package/src/state/app-activity.ts +7 -7
  402. package/src/state/app-bootstrap.ts +114 -40
  403. package/src/state/app-context-selector.test.tsx +70 -1
  404. package/src/state/app-context.test.ts +71 -3
  405. package/src/state/app-context.test.tsx +14 -7
  406. package/src/state/app-context.tsx +209 -869
  407. package/src/state/config-save-scheduler.ts +35 -0
  408. package/src/state/pane-scroll-registry.tsx +254 -0
  409. package/src/state/persist-scheduler.test.ts +67 -0
  410. package/src/state/persist-scheduler.ts +69 -0
  411. package/src/state/selectors-ui.ts +55 -0
  412. package/src/state/selectors.ts +15 -71
  413. package/src/state/session-persistence.test.ts +1 -1
  414. package/src/state/ticker-refresh-queue.ts +52 -3
  415. package/src/state/use-inline-tickers.ts +18 -16
  416. package/src/state/use-quote-streaming.test.ts +0 -6
  417. package/src/state/use-quote-streaming.test.tsx +1 -1
  418. package/src/state/use-quote-streaming.ts +3 -2
  419. package/src/state/use-session-snapshot.ts +32 -17
  420. package/src/test-support/plugin-runtime.ts +149 -0
  421. package/src/theme/color-utils.ts +1 -0
  422. package/src/theme/colors.ts +16 -0
  423. package/src/types/broker.ts +16 -1
  424. package/src/types/capability-route-source.ts +23 -0
  425. package/src/types/config.test.ts +23 -0
  426. package/src/types/config.ts +145 -10
  427. package/src/types/context-menu.ts +95 -0
  428. package/src/types/data-provider.ts +34 -3
  429. package/src/types/desktop-menu.ts +13 -0
  430. package/src/types/desktop-window.ts +29 -0
  431. package/src/types/financials.ts +123 -2
  432. package/src/types/instrument.ts +1 -0
  433. package/src/types/news-source.ts +9 -0
  434. package/src/types/persistence.ts +1 -0
  435. package/src/types/plugin.ts +32 -8
  436. package/src/ui/ascii-font.ts +85 -0
  437. package/src/ui/context-menu.test.ts +94 -0
  438. package/src/ui/context-menu.tsx +433 -0
  439. package/src/ui/dialog.tsx +55 -0
  440. package/src/ui/host.tsx +364 -0
  441. package/src/ui/index.tsx +134 -0
  442. package/src/ui/toast.tsx +40 -0
  443. package/src/updater.test.ts +69 -0
  444. package/src/updater.ts +61 -18
  445. package/src/utils/api-client.test.ts +99 -9
  446. package/src/utils/api-client.ts +378 -18
  447. package/src/utils/back-navigation.ts +6 -0
  448. package/src/utils/cli-output.ts +1 -0
  449. package/src/utils/currency-units.test.ts +1 -6
  450. package/src/utils/currency-units.ts +1 -0
  451. package/src/utils/debug-log.ts +68 -0
  452. package/src/utils/dom-screenshot.ts +326 -0
  453. package/src/utils/exchange-rate-map.ts +1 -0
  454. package/src/utils/exchanges.ts +214 -0
  455. package/src/utils/format.test.ts +7 -0
  456. package/src/utils/format.ts +1 -0
  457. package/src/utils/fuzzy-search.ts +1 -0
  458. package/src/utils/html-entities.test.ts +8 -0
  459. package/src/utils/html-entities.ts +32 -0
  460. package/src/utils/http-transport.ts +11 -0
  461. package/src/utils/main-thread-monitor.ts +127 -0
  462. package/src/utils/perf-marks.ts +54 -0
  463. package/src/utils/quote-freshness.ts +27 -50
  464. package/src/utils/quote-resolution.test.ts +5 -5
  465. package/src/utils/quote-resolution.ts +0 -1
  466. package/src/utils/quote-time.ts +1 -5
  467. package/src/utils/sec.test.ts +0 -12
  468. package/src/utils/selection-clipboard.ts +14 -6
  469. package/src/utils/shortcut-labels.test.ts +20 -0
  470. package/src/utils/shortcut-labels.ts +67 -0
  471. package/src/utils/slugify.ts +1 -0
  472. package/src/utils/terminal-input-reset.ts +1 -0
  473. package/src/utils/text-wrap.ts +82 -0
  474. package/src/utils/throttled-fetch.test.ts +19 -0
  475. package/src/utils/throttled-fetch.ts +11 -9
  476. package/src/utils/ticker-list.test.ts +1 -7
  477. package/src/utils/ticker-list.ts +1 -0
  478. package/src/utils/timestamp.ts +29 -0
  479. package/src/utils/tweet-text.test.ts +13 -0
  480. package/src/utils/tweet-text.ts +18 -0
  481. package/src/utils/url.ts +11 -0
  482. package/src/version.ts +1 -1
  483. package/src/components/chart/chart-control-hint.tsx +0 -39
  484. package/src/components/detail-feed-view.test.tsx +0 -180
  485. package/src/components/detail-feed-view.tsx +0 -531
  486. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  487. package/src/components/layout/pixel-mouse.test.ts +0 -40
  488. package/src/components/spinner.tsx +0 -2
  489. package/src/components/tab-bar.tsx +0 -17
  490. package/src/components/ui/multi-select-chips.tsx +0 -69
  491. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  492. package/src/plugins/builtin/ask-ai.tsx +0 -9
  493. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  494. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  495. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  496. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  497. package/src/plugins/builtin/fred-settings.ts +0 -10
  498. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  499. package/src/plugins/builtin/options-availability.ts +0 -158
  500. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  501. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  502. package/src/plugins/prediction-markets/status.ts +0 -24
  503. package/src/plugins/prediction-markets/table.tsx +0 -87
  504. package/src/utils/quote-time.test.ts +0 -22
  505. package/src/utils/selection-clipboard.test.ts +0 -43
  506. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,10 +1,16 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { useKeyboard } from "@opentui/react";
3
- import { useDialog } from "@opentui-ui/dialog/react";
1
+ import { Box, ScrollBox, Text } from "../../ui";
2
+ import { TextAttributes } from "../../ui";
3
+ import { useShortcut } from "../../react/input";
4
+ import { useDialog } from "../../ui/dialog";
4
5
  import { useCallback, useEffect, useState } from "react";
5
- import { PriceSelectorDialog } from "../../components";
6
+ import { PriceSelectorDialog, usePaneFooter } from "../../components";
6
7
  import { Button } from "../../components/ui/button";
7
- import { useAppState, usePaneCollection, usePaneInstanceId, usePaneTicker } from "../../state/app-context";
8
+ import {
9
+ useAppSelector,
10
+ usePaneCollection,
11
+ usePaneInstanceId,
12
+ usePaneTicker,
13
+ } from "../../state/app-context";
8
14
  import { colors, hoverBg } from "../../theme/colors";
9
15
  import type { DetailTabProps } from "../../types/plugin";
10
16
  import type { BrokerOrderRequest, BrokerOrderType } from "../../types/trading";
@@ -43,7 +49,8 @@ import {
43
49
  } from "./trade-utils";
44
50
 
45
51
  export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
46
- const { state } = useAppState();
52
+ const config = useAppSelector((state) => state.config);
53
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
47
54
  const paneId = usePaneInstanceId();
48
55
  const { collectionId } = usePaneCollection(paneId);
49
56
  const { ticker, financials } = usePaneTicker(paneId);
@@ -68,14 +75,14 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
68
75
  availableAccounts,
69
76
  gatewayRequiredMessage,
70
77
  } = useIbkrGatewaySelection(
71
- state.config,
72
- state.brokerAccounts,
78
+ config,
79
+ brokerAccounts,
73
80
  collectionId,
74
81
  preferredInstanceId,
75
82
  );
76
83
  const inferredAccountId = selectedInstance
77
84
  ? inferDraftAccountId(
78
- state.config,
85
+ config,
79
86
  collectionId,
80
87
  availableAccounts,
81
88
  selectedInstance.id,
@@ -138,7 +145,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
138
145
  useEffect(() => {
139
146
  if (!symbol || !ticker || !isGatewayMode || availableAccounts.length === 0 || ticketState.draft.accountId || !selectedInstance) return;
140
147
  const inferred = inferDraftAccountId(
141
- state.config,
148
+ config,
142
149
  collectionId,
143
150
  availableAccounts,
144
151
  selectedInstance.id,
@@ -154,7 +161,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
154
161
  availableAccounts,
155
162
  ticketState.draft.accountId,
156
163
  selectedInstance,
157
- state.config,
164
+ config,
158
165
  collectionId,
159
166
  tradeState.accountId,
160
167
  ]);
@@ -170,7 +177,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
170
177
  setTradeTicketBusy(symbol, true, ticker);
171
178
  await refreshGatewayData(selectedInstance);
172
179
  const inferred = inferDraftAccountId(
173
- state.config,
180
+ config,
174
181
  collectionId,
175
182
  availableAccounts,
176
183
  selectedInstance.id,
@@ -192,7 +199,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
192
199
  normalizedConfig,
193
200
  isGatewayMode,
194
201
  gatewayRequiredMessage,
195
- state.config,
202
+ config,
196
203
  collectionId,
197
204
  availableAccounts,
198
205
  tradeState.accountId,
@@ -222,14 +229,14 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
222
229
  choices={gatewayInstances.map((instance) => ({
223
230
  id: instance.id,
224
231
  label: instance.label,
225
- desc: "Gateway / TWS",
232
+ description: "Gateway / TWS",
226
233
  }))}
227
234
  />
228
235
  ),
229
236
  });
230
237
  if (!selected) return;
231
238
 
232
- const instance = getBrokerInstance(state.config.brokerInstances, selected);
239
+ const instance = getBrokerInstance(config.brokerInstances, selected);
233
240
  if (!instance) return;
234
241
  updateTradingPaneState({
235
242
  brokerInstanceId: instance.id,
@@ -253,7 +260,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
253
260
  lastError: undefined,
254
261
  lastInfo: undefined,
255
262
  }));
256
- }, [symbol, ticker, lockedBrokerInstanceId, gatewayInstances, dialog, state.config.brokerInstances]);
263
+ }, [symbol, ticker, lockedBrokerInstanceId, gatewayInstances, dialog, config.brokerInstances]);
257
264
 
258
265
  const chooseInstrument = useCallback(async () => {
259
266
  if (!symbol || !ticker) return;
@@ -292,7 +299,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
292
299
  const choices = results.slice(0, 12).map((result, index) => ({
293
300
  id: String(index),
294
301
  label: `${result.symbol} ${result.type}`.trim(),
295
- desc: `${selectedInstance.label} · ${result.name} · ${result.exchange || "SMART"}`,
302
+ description: `${selectedInstance.label} · ${result.name} · ${result.exchange || "SMART"}`,
296
303
  }));
297
304
  if (choices.length === 0) {
298
305
  setTradeTicketMessage(symbol, undefined, `No IBKR contracts matched "${search}".`, ticker);
@@ -322,7 +329,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
322
329
  }
323
330
 
324
331
  const nextAccounts = getKnownIbkrAccounts(
325
- state.brokerAccounts,
332
+ brokerAccounts,
326
333
  selectedInstance.id,
327
334
  gatewayService.getSnapshot().accounts,
328
335
  );
@@ -339,7 +346,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
339
346
  choices={nextAccounts.map((account) => ({
340
347
  id: account.accountId,
341
348
  label: `${selectedInstance.label} → ${account.accountId}`,
342
- desc: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
349
+ description: `${formatCurrency(account.netLiquidation || 0, account.currency || "USD")} net liq`,
343
350
  }))}
344
351
  />
345
352
  ),
@@ -355,6 +362,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
355
362
  gatewayService,
356
363
  isGatewayMode,
357
364
  availableAccounts,
365
+ brokerAccounts,
358
366
  refresh,
359
367
  dialog,
360
368
  ]);
@@ -427,10 +435,10 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
427
435
  {...ctx}
428
436
  title="Order Type"
429
437
  choices={[
430
- { id: "MKT", label: "Market", desc: "Execute at market price" },
431
- { id: "LMT", label: "Limit", desc: "Set a limit price" },
432
- { id: "STP", label: "Stop", desc: "Trigger at a stop price" },
433
- { id: "STP LMT", label: "Stop Limit", desc: "Trigger a limit order at a stop price" },
438
+ { id: "MKT", label: "Market", description: "Execute at market price" },
439
+ { id: "LMT", label: "Limit", description: "Set a limit price" },
440
+ { id: "STP", label: "Stop", description: "Trigger at a stop price" },
441
+ { id: "STP LMT", label: "Stop Limit", description: "Trigger a limit order at a stop price" },
434
442
  ]}
435
443
  />
436
444
  ),
@@ -495,7 +503,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
495
503
  updateTradingPaneState({ accountId: request.accountId });
496
504
  setTradeTicketDraft(symbol, { brokerInstanceId: selectedInstance.id, accountId: request.accountId }, ticker);
497
505
  setTradeTicketPreview(symbol, preview, ticker);
498
- setTradeTicketMessage(symbol, "Review the what-if preview, then press Enter again to submit.", undefined, ticker);
506
+ setTradeTicketMessage(symbol, "Review the what-if preview, then submit when ready.", undefined, ticker);
499
507
  } catch (error: any) {
500
508
  const message = error?.message || "Failed to preview order.";
501
509
  setTradeTicketMessage(symbol, undefined, message.replace("Timeout has occurred", "Preview timed out — try again."), ticker);
@@ -534,7 +542,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
534
542
  }
535
543
  }, [draftRequest, symbol, ticker, selectedInstance, normalizedConfig, gatewayService, ticketState.preview, ticketState.editingOrderId, previewOrder, refresh]);
536
544
 
537
- useKeyboard((event) => {
545
+ useShortcut((event) => {
538
546
  if (!focused || !symbol || !ticker) return;
539
547
 
540
548
  const isEnter = event.name === "enter" || event.name === "return";
@@ -605,14 +613,6 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
605
613
  }
606
614
  });
607
615
 
608
- if (!ticker || !symbol) {
609
- return (
610
- <box flexGrow={1} alignItems="center" justifyContent="center">
611
- <text fg={colors.textDim}>Select a ticker to draft an IBKR trade.</text>
612
- </box>
613
- );
614
- }
615
-
616
616
  const showLimit = isLimitOrder(ticketState.draft.orderType);
617
617
  const showStop = isStopOrder(ticketState.draft.orderType);
618
618
  const hasProfile = Boolean(selectedInstance);
@@ -628,14 +628,16 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
628
628
  ? Math.max(16, Math.floor((ticketPanelWidth - 5) / 4))
629
629
  : fieldWidth;
630
630
  const fieldTextWidth = Math.max(8, fieldWidth - 3);
631
- const activeContract = ticketState.draft.contract.symbol ? ticketState.draft.contract : normalizeContract(ticker);
631
+ const activeContract = ticketState.draft.contract.symbol
632
+ ? ticketState.draft.contract
633
+ : ticker ? normalizeContract(ticker) : ticketState.draft.contract;
632
634
  const hasContract = Boolean(activeContract.symbol);
633
635
  const hasAccount = Boolean(currentAccountId);
634
636
  const hasPreview = Boolean(ticketState.preview);
635
637
  const previewTextWidth = Math.max(18, (previewPanelWidth ?? Math.max(34, width - 6)) - 4);
636
638
  const contractValue = truncateText(formatContractLabel(activeContract), fieldTextWidth);
637
639
  const contractMeta = truncateText(
638
- ticketState.contractName || ticker.metadata.name || "Using current ticker",
640
+ ticketState.contractName || ticker?.metadata.name || "Using current ticker",
639
641
  Math.max(fieldTextWidth * Math.max(2, fieldsPerRow), 18),
640
642
  );
641
643
  const connectionTone: TradeTone = gatewaySnapshot.status.state === "connected"
@@ -654,7 +656,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
654
656
  || ticketState.lastInfo
655
657
  || gatewaySnapshot.status.message
656
658
  || gatewaySnapshot.lastError
657
- || "Click a workflow step or field to activate the ticket, then preview before submit.";
659
+ || "Choose a workflow step or field, then preview before submit.";
658
660
  const previewTone: TradeTone = !ticketState.preview
659
661
  ? "neutral"
660
662
  : ticketState.preview.warningText
@@ -683,8 +685,58 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
683
685
  ? "accent"
684
686
  : "neutral";
685
687
  const ticketHint = interactive
686
- ? "Esc releases the ticket. Field shortcuts stay active while captured."
687
- : "Click a field or press Enter to edit. Shortcuts are shown inline.";
688
+ ? "Field shortcuts stay active while captured."
689
+ : "Click a field to edit. Shortcuts are in the pane footer.";
690
+
691
+ usePaneFooter("ibkr-trade", () => ({
692
+ info: [
693
+ { id: "next", parts: [{ text: nextStep, tone: workflowTone === "positive" ? "positive" : workflowTone === "accent" ? "value" : "muted", bold: workflowTone !== "neutral" }] },
694
+ { id: "ticket", parts: [{ text: interactive ? "captured" : "standby", tone: interactive ? "positive" : "muted" }] },
695
+ ...(statusText ? [{ id: "status", parts: [{ text: statusText, tone: statusTone === "negative" ? "negative" as const : statusTone === "positive" ? "positive" as const : "muted" as const }] }] : []),
696
+ ],
697
+ hints: [
698
+ ...(!ticketState.busy ? [
699
+ { id: "refresh", key: "r", label: "efresh", onPress: () => refresh().catch(() => {}) },
700
+ { id: "profile", key: "i", label: "profile", onPress: () => chooseBrokerInstance().catch(() => {}) },
701
+ { id: "account", key: "a", label: "ccount", onPress: () => chooseAccount().catch(() => {}) },
702
+ ] : []),
703
+ ...(!ticketState.busy && symbol && ticker ? [
704
+ { id: "side", key: "b/v", label: "side", onPress: () => setTradeTicketDraft(symbol, { action: ticketState.draft.action === "BUY" ? "SELL" : "BUY" }, ticker) },
705
+ { id: "quantity", key: "q", label: "ty", onPress: () => editNumericField("Quantity", ticketState.draft.quantity, (value) => { if (value != null) setTradeTicketDraft(symbol, { quantity: value }, ticker); }).catch(() => {}) },
706
+ { id: "type", key: "t", label: "ype", onPress: () => editOrderType().catch(() => {}) },
707
+ ...(showLimit ? [{ id: "limit", key: "l", label: "imit", onPress: () => editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => { setTradeTicketDraft(symbol, { limitPrice: value }, ticker); }).catch(() => {}) }] : []),
708
+ ...(showStop ? [{ id: "stop", key: "x", label: "stop", onPress: () => editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => { setTradeTicketDraft(symbol, { stopPrice: value }, ticker); }).catch(() => {}) }] : []),
709
+ { id: "preview", key: "p", label: "review", onPress: () => previewOrder().catch(() => {}) },
710
+ ] : []),
711
+ ],
712
+ }), [
713
+ chooseAccount,
714
+ chooseBrokerInstance,
715
+ editNumericField,
716
+ editOrderType,
717
+ editPriceField,
718
+ interactive,
719
+ nextStep,
720
+ previewOrder,
721
+ refresh,
722
+ showLimit,
723
+ showStop,
724
+ statusText,
725
+ statusTone,
726
+ symbol,
727
+ ticketState.busy,
728
+ ticketState.draft,
729
+ ticker,
730
+ workflowTone,
731
+ ]);
732
+
733
+ if (!ticker || !symbol) {
734
+ return (
735
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
736
+ <Text fg={colors.textDim}>Select a ticker to draft an IBKR trade.</Text>
737
+ </Box>
738
+ );
739
+ }
688
740
 
689
741
  const renderFieldPill = ({
690
742
  id,
@@ -731,7 +783,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
731
783
  : valueColor ?? (disabled ? colors.textMuted : colors.text);
732
784
 
733
785
  return (
734
- <box
786
+ <Box
735
787
  key={id}
736
788
  width={itemWidth}
737
789
  minWidth={16}
@@ -741,18 +793,18 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
741
793
  paddingX={1}
742
794
  marginRight={1}
743
795
  onMouseMove={() => {
744
- if (!disabled) setHoveredField(id);
796
+ if (!disabled) setHoveredField((current) => (current === id ? current : id));
745
797
  }}
746
798
  onMouseDown={disabled ? undefined : () => {
747
799
  enterInteractive();
748
800
  onPress?.();
749
801
  }}
750
802
  >
751
- <text fg={labelColor}>{label}</text>
752
- <text fg={resolvedValueColor} attributes={valueAttributes}>
803
+ <Text fg={labelColor}>{label}</Text>
804
+ <Text fg={resolvedValueColor} attributes={valueAttributes}>
753
805
  {` ${truncateText(value, valueWidth)}`}
754
- </text>
755
- </box>
806
+ </Text>
807
+ </Box>
756
808
  );
757
809
  };
758
810
 
@@ -772,7 +824,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
772
824
  const palette = getTradeTonePalette(tone);
773
825
 
774
826
  return (
775
- <box
827
+ <Box
776
828
  key={id}
777
829
  height={1}
778
830
  flexDirection="row"
@@ -781,14 +833,14 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
781
833
  marginRight={1}
782
834
  onMouseDown={onPress}
783
835
  >
784
- <text fg={tone === "neutral" ? colors.textDim : palette.text}>{label}</text>
785
- <text fg={palette.text} attributes={TextAttributes.BOLD}>{` ${value}`}</text>
786
- </box>
836
+ <Text fg={tone === "neutral" ? colors.textDim : palette.text}>{label}</Text>
837
+ <Text fg={palette.text} attributes={TextAttributes.BOLD}>{` ${value}`}</Text>
838
+ </Box>
787
839
  );
788
840
  };
789
841
 
790
842
  const renderPreviewMetric = (label: string, value: string, tone: TradeTone = "neutral") => (
791
- <box
843
+ <Box
792
844
  key={label}
793
845
  width={previewMetricWidth}
794
846
  height={1}
@@ -796,35 +848,35 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
796
848
  paddingX={1}
797
849
  marginRight={1}
798
850
  >
799
- <text fg={tone === "negative" ? colors.negative : tone === "positive" ? colors.positive : colors.text}>
851
+ <Text fg={tone === "negative" ? colors.negative : tone === "positive" ? colors.positive : colors.text}>
800
852
  {truncateText(`${label} ${value}`, Math.max(6, previewMetricWidth - 2))}
801
- </text>
802
- </box>
853
+ </Text>
854
+ </Box>
803
855
  );
804
856
 
805
857
  return (
806
- <scrollbox flexGrow={1} scrollY>
807
- <box
858
+ <ScrollBox flexGrow={1} scrollY>
859
+ <Box
808
860
  flexDirection="column"
809
861
  paddingX={1}
810
862
  paddingBottom={1}
811
863
  gap={1}
812
864
  onMouseDown={!interactive ? enterInteractive : undefined}
813
865
  >
814
- <box flexDirection="row" flexWrap="wrap" justifyContent="space-between">
815
- <box flexDirection="column" marginBottom={1}>
816
- <box height={1} flexDirection="row">
817
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>{`Trade ${ticker.metadata.ticker}`}</text>
866
+ <Box flexDirection="row" flexWrap="wrap" justifyContent="space-between">
867
+ <Box flexDirection="column" marginBottom={1}>
868
+ <Box height={1} flexDirection="row">
869
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>{`Trade ${ticker.metadata.ticker}`}</Text>
818
870
  {ticker.metadata.name && ticker.metadata.name !== ticker.metadata.ticker && (
819
- <text fg={colors.textDim}>{` · ${ticker.metadata.name}`}</text>
871
+ <Text fg={colors.textDim}>{` · ${ticker.metadata.name}`}</Text>
820
872
  )}
821
- </box>
822
- <box height={1}>
823
- <text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote, { assetCategory: ticker.metadata.assetCategory })}</text>
824
- </box>
825
- </box>
873
+ </Box>
874
+ <Box height={1}>
875
+ <Text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote, { assetCategory: ticker.metadata.assetCategory })}</Text>
876
+ </Box>
877
+ </Box>
826
878
 
827
- <box flexDirection="row" flexWrap="wrap" justifyContent="flex-end">
879
+ <Box flexDirection="row" flexWrap="wrap" justifyContent="flex-end">
828
880
  <TradeBadge
829
881
  label="Broker"
830
882
  value={selectedInstance ? `${selectedInstance.label} ${isGatewayMode ? "Gateway" : "Flex"}` : "Select profile"}
@@ -852,10 +904,10 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
852
904
  chooseAccount().catch(() => {});
853
905
  }}
854
906
  />
855
- </box>
856
- </box>
907
+ </Box>
908
+ </Box>
857
909
 
858
- <box flexDirection="row" flexWrap="wrap">
910
+ <Box flexDirection="row" flexWrap="wrap">
859
911
  {renderSummaryPill({ id: "next", label: "Next", value: nextStep, tone: workflowTone })}
860
912
  {renderSummaryPill({
861
913
  id: "ticket",
@@ -866,21 +918,20 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
866
918
  })}
867
919
  <Button
868
920
  label="Refresh"
869
- shortcut="r"
870
921
  variant="ghost"
871
922
  disabled={ticketState.busy}
872
923
  onPress={() => refresh().catch(() => {})}
873
924
  />
874
- </box>
925
+ </Box>
875
926
 
876
- <box backgroundColor={getTradeTonePalette(statusTone).background} paddingX={1}>
877
- <text fg={ticketState.lastError ? colors.negative : ticketState.isSuccess ? colors.positive : colors.text}>
927
+ <Box backgroundColor={getTradeTonePalette(statusTone).background} paddingX={1}>
928
+ <Text fg={ticketState.lastError ? colors.negative : ticketState.isSuccess ? colors.positive : colors.text}>
878
929
  {statusText}
879
- </text>
880
- </box>
930
+ </Text>
931
+ </Box>
881
932
 
882
- <box flexDirection={wideLayout ? "row" : "column"} alignItems="stretch" gap={1}>
883
- <box
933
+ <Box flexDirection={wideLayout ? "row" : "column"} alignItems="stretch" gap={1}>
934
+ <Box
884
935
  flexDirection="column"
885
936
  flexGrow={1}
886
937
  width={wideLayout ? ticketPanelWidth : undefined}
@@ -889,17 +940,17 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
889
940
  borderColor={interactive ? colors.borderFocused : colors.border}
890
941
  paddingX={1}
891
942
  >
892
- <box height={1} flexDirection="row">
893
- <text fg={colors.textBright} attributes={TextAttributes.BOLD}>Ticket</text>
894
- <box flexGrow={1} />
895
- <text fg={interactive ? colors.positive : colors.textMuted}>
896
- {interactive ? "Captured" : "Click field / Enter"}
897
- </text>
898
- </box>
899
- <text fg={colors.textMuted}>{truncateText(ticketHint, Math.max(ticketPanelWidth - 4, 24))}</text>
900
- <box height={1} />
943
+ <Box height={1} flexDirection="row">
944
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Ticket</Text>
945
+ <Box flexGrow={1} />
946
+ <Text fg={interactive ? colors.positive : colors.textMuted}>
947
+ {interactive ? "Captured" : "Ready"}
948
+ </Text>
949
+ </Box>
950
+ <Text fg={colors.textMuted}>{truncateText(ticketHint, Math.max(ticketPanelWidth - 4, 24))}</Text>
951
+ <Box height={1} />
901
952
 
902
- <box flexDirection="row" flexWrap="wrap">
953
+ <Box flexDirection="row" flexWrap="wrap">
903
954
  {renderFieldPill({
904
955
  id: "profile",
905
956
  label: "Profile",
@@ -924,12 +975,12 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
924
975
  widthOverride: coreFieldWidth,
925
976
  onPress: () => chooseAccount().catch(() => {}),
926
977
  })}
927
- </box>
928
- <box height={1} />
929
- <box flexDirection="row" flexWrap="wrap">
978
+ </Box>
979
+ <Box height={1} />
980
+ <Box flexDirection="row" flexWrap="wrap">
930
981
  {renderFieldPill({
931
982
  id: "action",
932
- label: "Side [b/v]",
983
+ label: "Side",
933
984
  value: ticketState.draft.action,
934
985
  valueColor: ticketState.draft.action === "BUY" ? colors.positive : colors.negative,
935
986
  valueAttributes: TextAttributes.BOLD,
@@ -940,14 +991,14 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
940
991
  })}
941
992
  {renderFieldPill({
942
993
  id: "orderType",
943
- label: "Type [t]",
994
+ label: "Type",
944
995
  value: ticketState.draft.orderType,
945
996
  widthOverride: orderFieldWidth,
946
997
  onPress: () => editOrderType().catch(() => {}),
947
998
  })}
948
999
  {renderFieldPill({
949
1000
  id: "quantity",
950
- label: "Qty [q]",
1001
+ label: "Qty",
951
1002
  value: formatMarketQuantity(ticketState.draft.quantity, {
952
1003
  assetCategory: ticker.metadata.assetCategory,
953
1004
  contractSecType: activeContract.secType,
@@ -962,7 +1013,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
962
1013
  })}
963
1014
  {showLimit && renderFieldPill({
964
1015
  id: "limitPrice",
965
- label: "Limit [l]",
1016
+ label: "Limit",
966
1017
  value: ticketState.draft.limitPrice != null
967
1018
  ? formatMarketPrice(ticketState.draft.limitPrice, {
968
1019
  assetCategory: ticker.metadata.assetCategory,
@@ -979,7 +1030,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
979
1030
  })}
980
1031
  {showStop && renderFieldPill({
981
1032
  id: "stopPrice",
982
- label: "Stop [x]",
1033
+ label: "Stop",
983
1034
  value: ticketState.draft.stopPrice != null
984
1035
  ? formatMarketPrice(ticketState.draft.stopPrice, {
985
1036
  assetCategory: ticker.metadata.assetCategory,
@@ -1007,12 +1058,12 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
1007
1058
  valueColor: colors.textBright,
1008
1059
  widthOverride: orderFieldWidth,
1009
1060
  })}
1010
- </box>
1011
- <box height={1} />
1012
- <text fg={colors.textMuted}>{contractMeta}</text>
1013
- </box>
1061
+ </Box>
1062
+ <Box height={1} />
1063
+ <Text fg={colors.textMuted}>{contractMeta}</Text>
1064
+ </Box>
1014
1065
 
1015
- <box
1066
+ <Box
1016
1067
  flexDirection="column"
1017
1068
  width={previewPanelWidth}
1018
1069
  minWidth={34}
@@ -1021,16 +1072,16 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
1021
1072
  borderColor={getTradeTonePalette(previewTone).border}
1022
1073
  paddingX={1}
1023
1074
  >
1024
- <box height={1} flexDirection="row">
1025
- <text fg={colors.textBright} attributes={TextAttributes.BOLD}>Preview</text>
1026
- <box flexGrow={1} />
1027
- <text fg={getTradeTonePalette(previewTone).text}>{previewHeading}</text>
1028
- </box>
1029
- <text fg={ticketState.preview?.warningText ? colors.negative : colors.textMuted}>
1075
+ <Box height={1} flexDirection="row">
1076
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Preview</Text>
1077
+ <Box flexGrow={1} />
1078
+ <Text fg={getTradeTonePalette(previewTone).text}>{previewHeading}</Text>
1079
+ </Box>
1080
+ <Text fg={ticketState.preview?.warningText ? colors.negative : colors.textMuted}>
1030
1081
  {truncateText(formatPreviewSummary(ticketState.preview), previewTextWidth)}
1031
- </text>
1082
+ </Text>
1032
1083
 
1033
- <box flexDirection="row" flexWrap="wrap">
1084
+ <Box flexDirection="row" flexWrap="wrap">
1034
1085
  {renderPreviewMetric(
1035
1086
  "Fee",
1036
1087
  ticketState.preview?.commission != null
@@ -1041,28 +1092,26 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
1041
1092
  {renderPreviewMetric("Maint", formatPreviewMetric(ticketState.preview?.maintMarginBefore, ticketState.preview?.maintMarginAfter))}
1042
1093
  {renderPreviewMetric("Equity", formatPreviewMetric(ticketState.preview?.equityWithLoanBefore, ticketState.preview?.equityWithLoanAfter))}
1043
1094
  {ticketState.preview?.warningText && renderPreviewMetric("Warn", ticketState.preview.warningText, "negative")}
1044
- </box>
1095
+ </Box>
1045
1096
 
1046
- <box flexDirection="row" flexWrap="wrap">
1097
+ <Box flexDirection="row" flexWrap="wrap">
1047
1098
  <Button
1048
1099
  label="Preview"
1049
- shortcut="p"
1050
1100
  variant="secondary"
1051
1101
  disabled={ticketState.busy}
1052
1102
  onPress={() => previewOrder().catch(() => {})}
1053
1103
  />
1054
- <box width={1} />
1104
+ <Box width={1} />
1055
1105
  <Button
1056
1106
  label={ticketState.editingOrderId ? "Submit Change" : "Submit Order"}
1057
- shortcut="Enter"
1058
1107
  variant="primary"
1059
1108
  disabled={!ticketState.preview || ticketState.busy}
1060
1109
  onPress={() => submitOrder().catch(() => {})}
1061
1110
  />
1062
- </box>
1063
- </box>
1064
- </box>
1065
- </box>
1066
- </scrollbox>
1111
+ </Box>
1112
+ </Box>
1113
+ </Box>
1114
+ </Box>
1115
+ </ScrollBox>
1067
1116
  );
1068
1117
  }