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,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
  }