gloomberb 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -14
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +9 -10
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +52 -11
  87. package/src/components/layout/shell.test.tsx +315 -107
  88. package/src/components/layout/shell.tsx +1010 -368
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  180. package/src/plugins/builtin/chat-controller.ts +85 -14
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +363 -170
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +99 -9
  440. package/src/utils/api-client.ts +378 -18
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,16 +1,19 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { useTerminalDimensions } from "@opentui/react";
1
+ import { Box, ScrollBox, Text } from "../../../ui";
2
+ import { TextAttributes } from "../../../ui";
3
+ import { useViewport } from "../../../react/input";
3
4
  import { useFxRatesMap } from "../../../market-data/hooks";
4
5
  import { useAppSelector } from "../../../state/app-context";
5
- import { colors, priceColor, blendHex } from "../../../theme/colors";
6
+ import { colors, priceColor } from "../../../theme/colors";
6
7
  import {
7
8
  convertCurrency,
9
+ displayWidth,
8
10
  formatCompact,
9
11
  formatCompactCurrency,
10
12
  formatCurrency,
11
13
  formatNumber,
12
14
  formatPercent,
13
15
  formatPercentRaw,
16
+ padTo,
14
17
  } from "../../../utils/format";
15
18
  import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity, formatSignedMarketPrice } from "../../../utils/market-format";
16
19
  import {
@@ -22,123 +25,179 @@ import {
22
25
  import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
23
26
  import { EmptyState } from "../../../components";
24
27
  import { ResolvedStockChart } from "../../../components/chart/stock-chart";
25
- import type { TickerFinancials } from "../../../types/financials";
26
- import type { TickerRecord } from "../../../types/ticker";
28
+ import type { Quote, TickerFinancials } from "../../../types/financials";
29
+ import type { TickerPosition, TickerRecord } from "../../../types/ticker";
27
30
 
28
- // ─── Visual range bar ────────────────────────────────────────────────────────
31
+ const STAT_COLUMN_GAP = 2;
32
+ const STAT_LABEL_WIDTH = 12;
33
+ const BOOK_LABEL_WIDTH = 4;
34
+ const RANGE_ENDPOINT_WIDTH = 11;
35
+ const RANGE_MAX_WIDTH = 42;
36
+ const POSITION_COLUMN_GAP = 1;
29
37
 
30
- function RangeBar({
38
+ function CompactRangeBar({
31
39
  current,
32
40
  low,
33
41
  high,
34
42
  label,
35
- barWidth,
43
+ width,
36
44
  currency,
37
45
  assetCategory,
46
+ markerColor,
38
47
  }: {
39
48
  current: number;
40
49
  low: number;
41
50
  high: number;
42
51
  label: string;
43
- barWidth: number;
52
+ width: number;
44
53
  currency: string;
45
54
  assetCategory?: string;
55
+ markerColor: string;
46
56
  }) {
47
57
  const range = high - low;
48
58
  if (range <= 0) return null;
49
59
  const position = Math.max(0, Math.min(1, (current - low) / range));
50
- const filledWidth = Math.max(1, Math.round(position * barWidth));
51
- const emptyWidth = Math.max(0, barWidth - filledWidth);
52
60
  const pctLabel = `${Math.round(position * 100)}%`;
53
- const fillColor = blendHex(colors.negative, colors.positive, position);
54
- const trackChar = "─";
55
- const fillChar = "━";
61
+ const lowText = formatMarketPriceWithCurrency(low, currency, { assetCategory });
62
+ const highText = formatMarketPriceWithCurrency(high, currency, { assetCategory });
63
+ const endpointWidth = Math.min(
64
+ RANGE_ENDPOINT_WIDTH,
65
+ Math.max(7, Math.floor((width - 8) / 3)),
66
+ );
67
+ const barWidth = Math.max(5, width - endpointWidth * 2 - 2);
68
+ const markerIndex = Math.max(0, Math.min(barWidth - 1, Math.round(position * (barWidth - 1))));
69
+ const labelWidth = Math.max(0, width - displayWidth(pctLabel));
56
70
 
57
71
  return (
58
- <box flexDirection="column">
59
- <box flexDirection="row" height={1}>
60
- <box width={12}>
61
- <text fg={colors.textDim}>{label}</text>
62
- </box>
63
- <text fg={colors.textDim}>
64
- {formatMarketPriceWithCurrency(low, currency, { assetCategory })}
65
- </text>
66
- <box marginLeft={1} marginRight={1} flexDirection="row">
67
- <text fg={fillColor}>{fillChar.repeat(filledWidth)}</text>
68
- <text fg={colors.border}>{trackChar.repeat(emptyWidth)}</text>
69
- </box>
70
- <text fg={colors.textDim}>
71
- {formatMarketPriceWithCurrency(high, currency, { assetCategory })}
72
- </text>
73
- <box marginLeft={1}>
74
- <text fg={fillColor}>{pctLabel}</text>
75
- </box>
76
- </box>
77
- </box>
72
+ <Box flexDirection="column" width={width} flexShrink={0}>
73
+ <Box flexDirection="row" height={1}>
74
+ <Text fg={colors.textDim}>{padTo(label, labelWidth)}</Text>
75
+ <Text fg={markerColor}>{pctLabel}</Text>
76
+ </Box>
77
+ <Box flexDirection="row" height={1}>
78
+ <Text fg={colors.textDim}>
79
+ {padTo(lowText, endpointWidth)}
80
+ </Text>
81
+ <Box marginLeft={1} marginRight={1} width={barWidth} flexDirection="row">
82
+ <Text fg={colors.border}>{"─".repeat(markerIndex)}</Text>
83
+ <Text fg={markerColor}>{"●"}</Text>
84
+ <Text fg={colors.border}>{"─".repeat(Math.max(0, barWidth - markerIndex - 1))}</Text>
85
+ </Box>
86
+ <Text fg={colors.textDim}>
87
+ {padTo(highText, endpointWidth, "right")}
88
+ </Text>
89
+ </Box>
90
+ </Box>
78
91
  );
79
92
  }
80
93
 
81
- // ─── Volume bar ──────────────────────────────────────────────────────────────
94
+ function BookRow({
95
+ label,
96
+ value,
97
+ width,
98
+ valueColor,
99
+ }: {
100
+ label: string;
101
+ value: string;
102
+ width: number;
103
+ valueColor: string;
104
+ }) {
105
+ return (
106
+ <Box flexDirection="row" height={1} width={width}>
107
+ <Text fg={colors.textDim}>{padTo(label, BOOK_LABEL_WIDTH)}</Text>
108
+ <Text fg={valueColor}>{value}</Text>
109
+ </Box>
110
+ );
111
+ }
82
112
 
83
- function VolumeBar({ volume, avgVolume, barWidth }: { volume: number; avgVolume: number; barWidth: number }) {
84
- if (!volume || !avgVolume) return null;
85
- const ratio = volume / avgVolume;
86
- const maxRatio = Math.max(ratio, 1);
87
- const volWidth = Math.max(1, Math.round((Math.min(ratio, maxRatio) / maxRatio) * barWidth));
88
- const avgWidth = Math.max(1, Math.round((1 / maxRatio) * barWidth));
89
- const ratioColor = ratio >= 2 ? colors.textBright : ratio >= 1 ? colors.text : colors.textDim;
113
+ function QuoteBook({ quote, assetCategory, width }: { quote: Quote; assetCategory?: string; width: number }) {
114
+ const bidPrice = quote.bid != null
115
+ ? formatMarketPriceWithCurrency(quote.bid, quote.currency, { assetCategory })
116
+ : "—";
117
+ const askPrice = quote.ask != null
118
+ ? formatMarketPriceWithCurrency(quote.ask, quote.currency, { assetCategory })
119
+ : "—";
120
+ const bidText = quote.bidSize != null && quote.bidSize > 0 ? `${formatNumber(quote.bidSize, 0)} x ${bidPrice}` : bidPrice;
121
+ const askText = quote.askSize != null && quote.askSize > 0 ? `${formatNumber(quote.askSize, 0)} x ${askPrice}` : askPrice;
122
+ let spreadText = "—";
123
+ if (quote.bid != null && quote.ask != null) {
124
+ const spread = quote.ask - quote.bid;
125
+ const mid = (quote.ask + quote.bid) / 2;
126
+ const spreadPercent = mid > 0 ? ` (${((spread / mid) * 100).toFixed(2)}%)` : "";
127
+ spreadText = `${formatMarketPriceWithCurrency(spread, quote.currency, { assetCategory })}${spreadPercent}`;
128
+ }
90
129
 
91
130
  return (
92
- <box flexDirection="row" height={1}>
93
- <box width={12}>
94
- <text fg={colors.textDim}>Volume</text>
95
- </box>
96
- <text fg={colors.textDim}>{formatCompact(volume)}</text>
97
- <box marginLeft={1} marginRight={1} flexDirection="row">
98
- <text fg={ratioColor}>{"━".repeat(volWidth)}</text>
99
- <text fg={colors.border}>{"─".repeat(Math.max(0, barWidth - volWidth))}</text>
100
- </box>
101
- <text fg={colors.textDim}>avg {formatCompact(avgVolume)}</text>
102
- <box marginLeft={1}>
103
- <text fg={ratioColor}>{ratio.toFixed(1)}x</text>
104
- </box>
105
- </box>
131
+ <Box flexDirection="column" width={width} flexShrink={0}>
132
+ <BookRow label="Bid" value={bidText} width={width} valueColor={colors.borderFocused} />
133
+ <BookRow label="Ask" value={askText} width={width} valueColor={colors.negative} />
134
+ <BookRow label="Spr" value={spreadText} width={width} valueColor={colors.textDim} />
135
+ </Box>
106
136
  );
107
137
  }
108
138
 
109
- // ─── Two-column stat grid ────────────────────────────────────────────────────
110
-
111
139
  interface StatField {
112
140
  label: string;
113
141
  value: string;
114
142
  valueColor?: string;
115
143
  }
116
144
 
145
+ interface PositionTableRow {
146
+ account: string;
147
+ qty: string;
148
+ avg: string;
149
+ mark: string;
150
+ cost: string;
151
+ value: string;
152
+ pnl: string;
153
+ ret: string;
154
+ pnlValue: number | null;
155
+ }
156
+
157
+ interface PositionColumn {
158
+ key: keyof Omit<PositionTableRow, "pnlValue">;
159
+ label: string;
160
+ width: number;
161
+ align?: "left" | "right";
162
+ color?: (row: PositionTableRow) => string;
163
+ }
164
+
117
165
  function StatGrid({ fields, width }: { fields: StatField[]; width: number }) {
118
- const colWidth = Math.floor(width / 2);
119
- const rows: Array<[StatField | null, StatField | null]> = [];
120
- for (let i = 0; i < fields.length; i += 2) {
121
- rows.push([fields[i] ?? null, fields[i + 1] ?? null]);
166
+ const columnCount = width >= 58 ? 2 : 1;
167
+ const colWidth = Math.floor((width - STAT_COLUMN_GAP * (columnCount - 1)) / columnCount);
168
+ const rows: Array<Array<StatField | null>> = [];
169
+ for (let i = 0; i < fields.length; i += columnCount) {
170
+ rows.push(Array.from({ length: columnCount }, (_, offset) => fields[i + offset] ?? null));
122
171
  }
123
172
 
124
173
  return (
125
- <box flexDirection="column">
174
+ <Box flexDirection="column">
126
175
  {rows.map((row, i) => (
127
- <box key={i} flexDirection="row" height={1}>
176
+ <Box key={i} flexDirection="row" height={1}>
128
177
  {row.map((field, j) => {
129
- if (!field) return <box key={j} width={colWidth} />;
178
+ if (!field) {
179
+ return (
180
+ <Box key={j} flexDirection="row">
181
+ {j > 0 && <Box width={STAT_COLUMN_GAP} />}
182
+ <Box width={colWidth} />
183
+ </Box>
184
+ );
185
+ }
186
+ const labelWidth = Math.min(STAT_LABEL_WIDTH, Math.max(8, Math.floor(colWidth * 0.45)));
187
+ const valueWidth = Math.max(1, colWidth - labelWidth);
130
188
  return (
131
- <box key={j} width={colWidth} flexDirection="row">
132
- <box width={14}>
133
- <text fg={colors.textDim}>{field.label}</text>
134
- </box>
135
- <text fg={field.valueColor ?? colors.text}>{field.value}</text>
136
- </box>
189
+ <Box key={j} flexDirection="row">
190
+ {j > 0 && <Box width={STAT_COLUMN_GAP} />}
191
+ <Box width={colWidth} flexDirection="row">
192
+ <Text fg={colors.textDim}>{padTo(field.label, labelWidth)}</Text>
193
+ <Text fg={field.valueColor ?? colors.text}>{padTo(field.value, valueWidth, "right")}</Text>
194
+ </Box>
195
+ </Box>
137
196
  );
138
197
  })}
139
- </box>
198
+ </Box>
140
199
  ))}
141
- </box>
200
+ </Box>
142
201
  );
143
202
  }
144
203
 
@@ -146,9 +205,82 @@ function StatGrid({ fields, width }: { fields: StatField[]; width: number }) {
146
205
 
147
206
  function SectionHeader({ title }: { title: string }) {
148
207
  return (
149
- <box height={1}>
150
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>{title}</text>
151
- </box>
208
+ <Box height={1}>
209
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>{title}</Text>
210
+ </Box>
211
+ );
212
+ }
213
+
214
+ function compactPositionAccount(position: TickerPosition): string {
215
+ const rawAccount = position.brokerAccountId || position.portfolio;
216
+ const isBrokerPortfolio = rawAccount.startsWith("broker:");
217
+ const account = isBrokerPortfolio
218
+ ? rawAccount.split(":").filter(Boolean).at(-1) || rawAccount
219
+ : rawAccount;
220
+ const prefix = !isBrokerPortfolio && position.broker && position.broker !== "manual" ? `${position.broker} ` : "";
221
+ const suffix = position.side === "short" ? " SHORT" : "";
222
+ return `${prefix}${account}${suffix}`;
223
+ }
224
+
225
+ function createPositionColumns(width: number): PositionColumn[] {
226
+ const columns: PositionColumn[] = width >= 84
227
+ ? [
228
+ { key: "account", label: "Account", width: 0 },
229
+ { key: "qty", label: "Qty", width: 8, align: "right" },
230
+ { key: "avg", label: "Avg", width: 9, align: "right" },
231
+ { key: "mark", label: "Mark", width: 9, align: "right" },
232
+ { key: "cost", label: "Cost", width: 11, align: "right" },
233
+ { key: "value", label: "Value", width: 11, align: "right" },
234
+ { key: "pnl", label: "P&L", width: 12, align: "right", color: (row) => priceColor(row.pnlValue ?? 0) },
235
+ { key: "ret", label: "Ret", width: 7, align: "right", color: (row) => priceColor(row.pnlValue ?? 0) },
236
+ ]
237
+ : width >= 70
238
+ ? [
239
+ { key: "account", label: "Account", width: 0 },
240
+ { key: "qty", label: "Qty", width: 8, align: "right" },
241
+ { key: "avg", label: "Avg", width: 9, align: "right" },
242
+ { key: "mark", label: "Mark", width: 9, align: "right" },
243
+ { key: "value", label: "Value", width: 11, align: "right" },
244
+ { key: "pnl", label: "P&L", width: 12, align: "right", color: (row) => priceColor(row.pnlValue ?? 0) },
245
+ ]
246
+ : [
247
+ { key: "account", label: "Account", width: 0 },
248
+ { key: "qty", label: "Qty", width: 8, align: "right" },
249
+ { key: "value", label: "Value", width: 11, align: "right" },
250
+ { key: "pnl", label: "P&L", width: 12, align: "right", color: (row) => priceColor(row.pnlValue ?? 0) },
251
+ ];
252
+ const fixedWidth = columns.reduce((sum, column) => sum + column.width, 0) + POSITION_COLUMN_GAP * (columns.length - 1);
253
+ const accountColumn = columns[0]!;
254
+ accountColumn.width = Math.max(8, width - fixedWidth);
255
+ return columns;
256
+ }
257
+
258
+ function PositionTable({ rows, width }: { rows: PositionTableRow[]; width: number }) {
259
+ const columns = createPositionColumns(width);
260
+
261
+ return (
262
+ <Box flexDirection="column" width={width}>
263
+ <Box flexDirection="row" height={1}>
264
+ {columns.map((column, index) => (
265
+ <Box key={column.key} flexDirection="row">
266
+ {index > 0 && <Box width={POSITION_COLUMN_GAP} />}
267
+ <Text fg={colors.textDim}>{padTo(column.label, column.width, column.align)}</Text>
268
+ </Box>
269
+ ))}
270
+ </Box>
271
+ {rows.map((row, rowIndex) => (
272
+ <Box key={rowIndex} flexDirection="row" height={1}>
273
+ {columns.map((column, index) => (
274
+ <Box key={column.key} flexDirection="row">
275
+ {index > 0 && <Box width={POSITION_COLUMN_GAP} />}
276
+ <Text fg={column.color?.(row) ?? (column.key === "account" ? colors.textBright : colors.text)}>
277
+ {padTo(row[column.key], column.width, column.align)}
278
+ </Text>
279
+ </Box>
280
+ ))}
281
+ </Box>
282
+ ))}
283
+ </Box>
152
284
  );
153
285
  }
154
286
 
@@ -167,7 +299,7 @@ export function OverviewTab({
167
299
  }) {
168
300
  const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
169
301
  const exchangeRatesState = useAppSelector((state) => state.exchangeRates);
170
- const { width: termWidth } = useTerminalDimensions();
302
+ const { width: termWidth } = useViewport();
171
303
 
172
304
  if (!ticker) return <EmptyState title="No ticker selected." />;
173
305
 
@@ -194,20 +326,36 @@ export function OverviewTab({
194
326
  const routingVenue = exchangeShortName(quote?.routingExchangeName, quote?.routingExchangeFullName);
195
327
  const priceSource = quote?.provenance?.price ? quoteSourceLabel(quote.provenance.price, "price") : "";
196
328
  const sessionSource = quote?.provenance?.session ? quoteSourceLabel(quote.provenance.session, "session") : "";
329
+ const sourceSummary = priceSource && sessionSource && priceSource !== sessionSource
330
+ ? `src ${priceSource}/${sessionSource}`
331
+ : priceSource || sessionSource
332
+ ? `src ${priceSource || sessionSource}`
333
+ : "";
197
334
  const metadataParts = [
198
- priceSource ? `Price source: ${priceSource}` : "",
199
- sessionSource ? `Session source: ${sessionSource}` : "",
200
- routingVenue && routingVenue !== listingVenue ? `Route: ${routingVenue}` : "",
335
+ sourceSummary,
336
+ routingVenue && routingVenue !== listingVenue ? `route ${routingVenue}` : "",
201
337
  ].filter((part) => part.length > 0);
202
338
 
203
339
  const contentWidth = Math.max((width || Math.floor(termWidth * 0.5)) - 4, 20);
204
340
  const chartWidth = contentWidth;
205
- const barWidth = Math.max(10, contentWidth - 50);
206
341
  const hasHistory = (financials?.priceHistory?.length ?? 0) > 2;
342
+ const hasBidAsk = quote?.bid != null || quote?.ask != null;
343
+ const quoteBookInline = hasBidAsk && contentWidth >= 68;
344
+ const quoteBookWidth = quoteBookInline ? Math.min(32, Math.max(24, Math.floor(contentWidth * 0.3))) : Math.min(contentWidth, 32);
345
+ const quoteSummaryWidth = quoteBookInline ? Math.max(20, contentWidth - quoteBookWidth - 2) : contentWidth;
346
+ const hasDayRange = quote?.low != null && quote?.high != null && quote.high > quote.low;
347
+ const hasYearRange = quote?.low52w != null && quote?.high52w != null && quote.high52w > quote.low52w;
348
+ const rangeInline = contentWidth >= 70 && hasDayRange && hasYearRange;
349
+ const rangeWidth = rangeInline
350
+ ? Math.min(RANGE_MAX_WIDTH, Math.floor((contentWidth - 2) / 2))
351
+ : Math.min(RANGE_MAX_WIDTH, contentWidth);
352
+ const rangeMarkerColor = quote ? priceColor(quote.change) : colors.textDim;
207
353
 
208
- // Build the two-column stat grid
209
354
  const stats: StatField[] = [];
210
355
 
356
+ if (quote?.volume != null) {
357
+ stats.push({ label: "Volume", value: formatCompact(quote.volume) });
358
+ }
211
359
  if (quote?.marketCap) {
212
360
  stats.push({ label: "Market Cap", value: formatCompactCurrency(toBase(quote.marketCap, quoteCurrency), baseCurrency) });
213
361
  }
@@ -269,103 +417,143 @@ export function OverviewTab({
269
417
  stats.push({ label: "EV", value: formatCompact(fundamentals.enterpriseValue) });
270
418
  }
271
419
 
272
- // Bid/ask
273
- const hasBidAsk = quote?.bid != null || quote?.ask != null;
420
+ const positionRows: PositionTableRow[] = ticker.metadata.positions.map((position) => {
421
+ const positionCurrency = position.currency || quoteCurrency;
422
+ const costBasis = position.shares * position.avgCost * (position.multiplier || 1);
423
+ const costBasisBase = toBase(costBasis, positionCurrency);
424
+ const fallbackMarkPrice = position.markPrice ?? quote?.price;
425
+ const fallbackMarkCurrency = position.markPrice != null ? positionCurrency : quoteCurrency;
426
+ const marketValueBase = position.marketValue != null
427
+ ? toBase(position.marketValue, positionCurrency)
428
+ : fallbackMarkPrice != null
429
+ ? toBase(Math.abs(position.shares) * fallbackMarkPrice * (position.multiplier || 1), fallbackMarkCurrency)
430
+ : null;
431
+ const pnlValue = position.unrealizedPnl != null
432
+ ? toBase(position.unrealizedPnl, positionCurrency)
433
+ : marketValueBase != null
434
+ ? marketValueBase - costBasisBase
435
+ : null;
436
+ const returnPercent = pnlValue != null && costBasisBase !== 0
437
+ ? formatPercentRaw((pnlValue / Math.abs(costBasisBase)) * 100)
438
+ : "—";
439
+ const unit = position.multiplier && position.multiplier > 1 ? " ct" : " sh";
440
+
441
+ return {
442
+ account: compactPositionAccount(position),
443
+ qty: `${formatMarketQuantity(position.shares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier })}${unit}`,
444
+ avg: formatMarketCostWithCurrency(position.avgCost, positionCurrency, {
445
+ assetCategory: ticker.metadata.assetCategory,
446
+ multiplier: position.multiplier,
447
+ }),
448
+ mark: fallbackMarkPrice != null
449
+ ? formatMarketPriceWithCurrency(fallbackMarkPrice, fallbackMarkCurrency, {
450
+ assetCategory: ticker.metadata.assetCategory,
451
+ multiplier: position.multiplier,
452
+ })
453
+ : "—",
454
+ cost: formatCurrency(costBasisBase, baseCurrency),
455
+ value: marketValueBase != null ? formatCurrency(marketValueBase, baseCurrency) : "—",
456
+ pnl: pnlValue != null ? `${pnlValue >= 0 ? "+" : ""}${formatCurrency(pnlValue, baseCurrency)}` : "—",
457
+ ret: returnPercent,
458
+ pnlValue,
459
+ };
460
+ });
274
461
 
275
462
  return (
276
- <scrollbox flexGrow={1} scrollY>
277
- <box flexDirection="column" paddingX={1} paddingBottom={1} gap={1}>
278
- {/* Ticker header */}
279
- <box flexDirection="row">
280
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>
281
- {ticker.metadata.ticker}
282
- </text>
283
- {ticker.metadata.name && ticker.metadata.name !== ticker.metadata.ticker && (
284
- <text fg={colors.textDim}>
285
- {" "}- {ticker.metadata.name || quote?.name || ""}
286
- </text>
287
- )}
288
- {listingVenue && (
289
- <text fg={colors.textDim}>{" "}({listingVenue})</text>
290
- )}
291
- {quote?.marketState && (
292
- <text fg={marketStateColor(quote.marketState)}>
293
- {" "}{marketStateLabel(quote.marketState)}
294
- </text>
295
- )}
296
- </box>
297
-
298
- {/* Price block */}
299
- {quote && (
300
- <box flexDirection="column" gap={0}>
301
- <box flexDirection="row" gap={2}>
302
- <text attributes={TextAttributes.BOLD} fg={priceColor(quote.change)}>
303
- {formatMarketPriceWithCurrency(quote.price, quote.currency, { assetCategory: ticker.metadata.assetCategory })}
304
- </text>
305
- <text fg={priceColor(quote.change)}>
306
- {formatSignedMarketPrice(quote.change, { assetCategory: ticker.metadata.assetCategory })} ({formatPercentRaw(quote.changePercent)})
307
- </text>
308
- </box>
309
- {(quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null && (
310
- <box flexDirection="row" gap={2}>
311
- <text fg={colors.textDim}>Pre-Market:</text>
312
- <text fg={priceColor(quote.preMarketChange ?? 0)}>
463
+ <ScrollBox flexGrow={1} flexBasis={0} scrollY focusable={false}>
464
+ <Box flexDirection="column" paddingX={1} paddingBottom={1} gap={1}>
465
+ <Box flexDirection={quoteBookInline ? "row" : "column"} gap={quoteBookInline ? 2 : 0} width={contentWidth}>
466
+ <Box flexDirection="column" width={quoteSummaryWidth}>
467
+ <Box flexDirection="row">
468
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
469
+ {ticker.metadata.ticker}
470
+ </Text>
471
+ {ticker.metadata.name && ticker.metadata.name !== ticker.metadata.ticker && (
472
+ <Text fg={colors.textDim}>
473
+ {" "}- {ticker.metadata.name || quote?.name || ""}
474
+ </Text>
475
+ )}
476
+ {listingVenue && (
477
+ <Text fg={colors.textDim}>{" "}({listingVenue})</Text>
478
+ )}
479
+ {quote?.marketState && (
480
+ <Text fg={marketStateColor(quote.marketState)}>
481
+ {" "}{marketStateLabel(quote.marketState)}
482
+ </Text>
483
+ )}
484
+ </Box>
485
+
486
+ {quote && (
487
+ <Box flexDirection="row" gap={2}>
488
+ <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
489
+ {formatMarketPriceWithCurrency(quote.price, quote.currency, { assetCategory: ticker.metadata.assetCategory })}
490
+ </Text>
491
+ <Text fg={priceColor(quote.change)}>
492
+ {formatSignedMarketPrice(quote.change, { assetCategory: ticker.metadata.assetCategory })} ({formatPercentRaw(quote.changePercent)})
493
+ </Text>
494
+ </Box>
495
+ )}
496
+ {quote && (quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null && (
497
+ <Box flexDirection="row" gap={2}>
498
+ <Text fg={colors.textDim}>Pre-Market:</Text>
499
+ <Text fg={priceColor(quote.preMarketChange ?? 0)}>
313
500
  {formatMarketPriceWithCurrency(quote.preMarketPrice, quote.currency, { assetCategory: ticker.metadata.assetCategory })}
314
- </text>
315
- <text fg={priceColor(quote.preMarketChange ?? 0)}>
501
+ </Text>
502
+ <Text fg={priceColor(quote.preMarketChange ?? 0)}>
316
503
  {formatSignedMarketPrice(quote.preMarketChange ?? 0, { assetCategory: ticker.metadata.assetCategory })} ({formatPercentRaw(quote.preMarketChangePercent ?? 0)})
317
- </text>
318
- </box>
504
+ </Text>
505
+ </Box>
319
506
  )}
320
- {(quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null && (
321
- <box flexDirection="row" gap={2}>
322
- <text fg={colors.textDim}>After-Hours:</text>
323
- <text fg={priceColor(quote.postMarketChange ?? 0)}>
507
+ {quote && (quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null && (
508
+ <Box flexDirection="row" gap={2}>
509
+ <Text fg={colors.textDim}>After-Hours:</Text>
510
+ <Text fg={priceColor(quote.postMarketChange ?? 0)}>
324
511
  {formatMarketPriceWithCurrency(quote.postMarketPrice, quote.currency, { assetCategory: ticker.metadata.assetCategory })}
325
- </text>
326
- <text fg={priceColor(quote.postMarketChange ?? 0)}>
512
+ </Text>
513
+ <Text fg={priceColor(quote.postMarketChange ?? 0)}>
327
514
  {formatSignedMarketPrice(quote.postMarketChange ?? 0, { assetCategory: ticker.metadata.assetCategory })} ({formatPercentRaw(quote.postMarketChangePercent ?? 0)})
328
- </text>
329
- </box>
515
+ </Text>
516
+ </Box>
330
517
  )}
331
518
  {metadataParts.length > 0 && (
332
- <text fg={colors.textDim}>{metadataParts.join(" | ")}</text>
519
+ <Text fg={colors.textDim}>{metadataParts.join(" | ")}</Text>
333
520
  )}
334
- </box>
335
- )}
521
+ </Box>
336
522
 
337
- {/* Range bars */}
338
- {quote?.low != null && quote?.high != null && quote.high > quote.low && (
339
- <RangeBar
340
- current={quote.price}
341
- low={quote.low}
342
- high={quote.high}
343
- label="Day Range"
344
- barWidth={barWidth}
345
- currency={quoteCurrency}
346
- assetCategory={ticker.metadata.assetCategory}
347
- />
348
- )}
349
- {quote?.low52w != null && quote?.high52w != null && quote.high52w > quote.low52w && (
350
- <RangeBar
351
- current={quote.price}
352
- low={quote.low52w}
353
- high={quote.high52w}
354
- label="52W Range"
355
- barWidth={barWidth}
356
- currency={quoteCurrency}
357
- assetCategory={ticker.metadata.assetCategory}
358
- />
359
- )}
360
- {quote?.volume != null && fundamentals?.sharesOutstanding && (
361
- <VolumeBar
362
- volume={quote.volume}
363
- avgVolume={Math.round((fundamentals.sharesOutstanding * 0.01) || quote.volume)}
364
- barWidth={barWidth}
365
- />
523
+ {quote && hasBidAsk && (
524
+ <QuoteBook quote={quote} assetCategory={ticker.metadata.assetCategory} width={quoteBookWidth} />
525
+ )}
526
+ </Box>
527
+
528
+ {(hasDayRange || hasYearRange) && quote && (
529
+ <Box flexDirection={rangeInline ? "row" : "column"} gap={rangeInline ? 2 : 0} width={contentWidth}>
530
+ {hasDayRange && (
531
+ <CompactRangeBar
532
+ current={quote.price}
533
+ low={quote.low!}
534
+ high={quote.high!}
535
+ label="Day Range"
536
+ width={rangeWidth}
537
+ currency={quoteCurrency}
538
+ assetCategory={ticker.metadata.assetCategory}
539
+ markerColor={rangeMarkerColor}
540
+ />
541
+ )}
542
+ {hasYearRange && (
543
+ <CompactRangeBar
544
+ current={quote.price}
545
+ low={quote.low52w!}
546
+ high={quote.high52w!}
547
+ label="52W Range"
548
+ width={rangeWidth}
549
+ currency={quoteCurrency}
550
+ assetCategory={ticker.metadata.assetCategory}
551
+ markerColor={rangeMarkerColor}
552
+ />
553
+ )}
554
+ </Box>
366
555
  )}
367
556
 
368
- {/* Chart */}
369
557
  {hasHistory && (
370
558
  <ResolvedStockChart
371
559
  width={chartWidth}
@@ -378,136 +566,60 @@ export function OverviewTab({
378
566
  />
379
567
  )}
380
568
 
381
- {/* Bid/Ask */}
382
- {hasBidAsk && quote && (
383
- <box flexDirection="row" height={1} gap={3}>
384
- <box flexDirection="row">
385
- <text fg={colors.textDim}>Bid: </text>
386
- <text fg={colors.text}>
387
- {quote.bid != null ? formatMarketPriceWithCurrency(quote.bid, quote.currency, { assetCategory: ticker.metadata.assetCategory }) : "—"}
388
- {quote.bidSize != null ? ` ×${quote.bidSize}` : ""}
389
- </text>
390
- </box>
391
- <box flexDirection="row">
392
- <text fg={colors.textDim}>Ask: </text>
393
- <text fg={colors.text}>
394
- {quote.ask != null ? formatMarketPriceWithCurrency(quote.ask, quote.currency, { assetCategory: ticker.metadata.assetCategory }) : "—"}
395
- {quote.askSize != null ? ` ×${quote.askSize}` : ""}
396
- </text>
397
- </box>
398
- {quote.bid != null && quote.ask != null && (
399
- <box flexDirection="row">
400
- <text fg={colors.textDim}>Spread: </text>
401
- <text fg={colors.text}>
402
- {formatMarketPriceWithCurrency(quote.ask - quote.bid, quote.currency, { assetCategory: ticker.metadata.assetCategory })}
403
- </text>
404
- </box>
405
- )}
406
- </box>
407
- )}
408
-
409
- {/* Two-column stats grid */}
410
569
  {stats.length > 0 && (
411
- <box flexDirection="column">
570
+ <Box flexDirection="column">
412
571
  <SectionHeader title="Fundamentals" />
413
572
  <StatGrid fields={stats} width={contentWidth} />
414
- </box>
573
+ </Box>
415
574
  )}
416
575
 
417
- {/* Positions */}
418
- {ticker.metadata.positions.length > 0 && (
419
- <box flexDirection="column">
576
+ {positionRows.length > 0 && (
577
+ <Box flexDirection="column">
420
578
  <SectionHeader title="Positions" />
421
- {ticker.metadata.positions.map((position, index) => {
422
- const costBasis = position.shares * position.avgCost * (position.multiplier || 1);
423
- const positionCurrency = position.currency || quoteCurrency;
424
- const costBasisBase = toBase(costBasis, positionCurrency);
425
- const marketValueBase = position.marketValue != null
426
- ? toBase(position.marketValue, positionCurrency)
427
- : position.markPrice != null
428
- ? toBase(Math.abs(position.shares) * position.markPrice * (position.multiplier || 1), positionCurrency)
429
- : quote
430
- ? toBase(Math.abs(position.shares) * quote.price * (position.multiplier || 1), quoteCurrency)
431
- : null;
432
- const pnlValue = position.unrealizedPnl != null
433
- ? toBase(position.unrealizedPnl, positionCurrency)
434
- : marketValueBase != null
435
- ? marketValueBase - costBasisBase
436
- : null;
437
- const pnlText = pnlValue != null
438
- ? ` P&L: ${pnlValue >= 0 ? "+" : ""}${formatCurrency(pnlValue, baseCurrency)}`
439
- : "";
440
-
441
- return (
442
- <box key={index} flexDirection="column">
443
- <box flexDirection="row" height={1}>
444
- <text fg={colors.textDim}>{position.portfolio}</text>
445
- <text fg={colors.textMuted}>{" via "}{position.broker}</text>
446
- {position.side === "short" && <text fg={colors.negative}>{" SHORT"}</text>}
447
- </box>
448
- <box flexDirection="row" height={1}>
449
- <text fg={colors.text}>
450
- {formatMarketQuantity(position.shares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier })} {position.multiplier && position.multiplier > 1 ? "contracts" : "shares"} @ {formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier })}
451
- {" = "}{formatCurrency(costBasisBase, baseCurrency)}
452
- </text>
453
- {pnlText && (
454
- <text fg={priceColor(pnlValue ?? 0)}>{pnlText}</text>
455
- )}
456
- </box>
457
- {position.markPrice != null && (
458
- <box flexDirection="row" height={1}>
459
- <text fg={colors.textDim}>Mark: {formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier })}</text>
460
- {marketValueBase != null && (
461
- <text fg={colors.textDim}>{" "}Mkt Value: {formatCurrency(marketValueBase, baseCurrency)}</text>
462
- )}
463
- </box>
464
- )}
465
- </box>
466
- );
467
- })}
468
- </box>
579
+ <PositionTable rows={positionRows} width={contentWidth} />
580
+ </Box>
469
581
  )}
470
582
 
471
583
  {/* Sector / Industry / Type */}
472
584
  {(sector || industry || ticker.metadata.assetCategory) && (
473
- <box flexDirection="row" height={1} gap={3}>
585
+ <Box flexDirection="row" height={1} gap={3}>
474
586
  {ticker.metadata.assetCategory && (
475
- <box flexDirection="row">
476
- <text fg={colors.textDim}>Type: </text>
477
- <text fg={colors.text}>{ticker.metadata.assetCategory}</text>
478
- </box>
587
+ <Box flexDirection="row">
588
+ <Text fg={colors.textDim}>Type: </Text>
589
+ <Text fg={colors.text}>{ticker.metadata.assetCategory}</Text>
590
+ </Box>
479
591
  )}
480
592
  {sector && (
481
- <box flexDirection="row">
482
- <text fg={colors.textDim}>Sector: </text>
483
- <text fg={colors.text}>{sector}</text>
484
- </box>
593
+ <Box flexDirection="row">
594
+ <Text fg={colors.textDim}>Sector: </Text>
595
+ <Text fg={colors.text}>{sector}</Text>
596
+ </Box>
485
597
  )}
486
598
  {industry && (
487
- <box flexDirection="row">
488
- <text fg={colors.textDim}>Industry: </text>
489
- <text fg={colors.text}>{industry}</text>
490
- </box>
599
+ <Box flexDirection="row">
600
+ <Text fg={colors.textDim}>Industry: </Text>
601
+ <Text fg={colors.text}>{industry}</Text>
602
+ </Box>
491
603
  )}
492
- </box>
604
+ </Box>
493
605
  )}
494
606
 
495
607
  {/* ISIN */}
496
608
  {ticker.metadata.isin && (
497
- <box flexDirection="row" height={1}>
498
- <text fg={colors.textDim}>ISIN: </text>
499
- <text fg={colors.text}>{ticker.metadata.isin}</text>
500
- </box>
609
+ <Box flexDirection="row" height={1}>
610
+ <Text fg={colors.textDim}>ISIN: </Text>
611
+ <Text fg={colors.text}>{ticker.metadata.isin}</Text>
612
+ </Box>
501
613
  )}
502
614
 
503
615
  {/* Description — last, collapsed */}
504
616
  {description && (
505
- <box flexDirection="column">
617
+ <Box flexDirection="column" width={contentWidth}>
506
618
  <SectionHeader title="Description" />
507
- <text fg={colors.text}>{description}</text>
508
- </box>
619
+ <Text fg={colors.text} width={contentWidth} wrapMode="word" wrapText>{description}</Text>
620
+ </Box>
509
621
  )}
510
- </box>
511
- </scrollbox>
622
+ </Box>
623
+ </ScrollBox>
512
624
  );
513
625
  }