gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,4 +1,4 @@
1
- import { RGBA } from "@opentui/core";
1
+ import { RGBA } from "../../ui";
2
2
  import type { ChartAxisMode, ChartColors, ChartIndicatorOverlays, Pixel, PixelBuffer, ChartRenderMode } from "./chart-types";
3
3
  import type { ProjectedChartPoint } from "./chart-data";
4
4
  import { formatCompactMarketPriceWithCurrency, formatMarketPrice, resolveAssetDisplayKind } from "../../utils/market-format";
@@ -29,6 +29,27 @@ const LAYER_DATA = 2;
29
29
  const LAYER_OVERLAY = 2;
30
30
  const LAYER_CROSSHAIR = 3;
31
31
 
32
+ function normalizeCount(value: number, min: number): number {
33
+ if (!Number.isFinite(value)) return min;
34
+ return Math.max(Math.floor(value), min);
35
+ }
36
+
37
+ function normalizeRenderDimensions(opts: RenderChartOptions) {
38
+ const width = normalizeCount(opts.width, 1);
39
+ const height = normalizeCount(opts.height, 1);
40
+ const volumeHeight = opts.showVolume
41
+ ? Math.min(normalizeCount(opts.volumeHeight, 0), Math.max(height - 1, 0))
42
+ : 0;
43
+ const showVolume = opts.showVolume && volumeHeight > 0;
44
+ return {
45
+ width,
46
+ height,
47
+ showVolume,
48
+ volumeHeight,
49
+ chartRows: Math.max(height - volumeHeight, 1),
50
+ };
51
+ }
52
+
32
53
  // ---------------------------------------------------------------------------
33
54
  // Braille character mapping
34
55
  // Each terminal cell maps to a 2×4 dot grid. Unicode braille = U+2800 + bits.
@@ -70,14 +91,15 @@ export interface ResolvedChartPalette extends ChartColors {
70
91
 
71
92
  type VolumeTrendMode = "previousClose" | "openClose";
72
93
 
73
- function makeChunk(text: string, fgColor: string, bgColor: string): StyledChunk {
74
- return {
94
+ function makeChunk(text: string, fgColor?: string, bgColor?: string): StyledChunk {
95
+ const chunk: StyledChunk = {
75
96
  __isChunk: true,
76
97
  text,
77
- fg: RGBA.fromHex(fgColor),
78
- bg: RGBA.fromHex(bgColor),
79
98
  attributes: 0,
80
99
  };
100
+ if (fgColor) chunk.fg = RGBA.fromHex(fgColor);
101
+ if (bgColor) chunk.bg = RGBA.fromHex(bgColor);
102
+ return chunk;
81
103
  }
82
104
 
83
105
  function blendHex(a: string, b: string, ratio: number): string {
@@ -125,11 +147,13 @@ export function resolveChartPalette(
125
147
  // ---------------------------------------------------------------------------
126
148
 
127
149
  export function createPixelBuffer(width: number, heightPixels: number): PixelBuffer {
150
+ const bufferWidth = normalizeCount(width, 0);
151
+ const bufferHeight = normalizeCount(heightPixels, 0);
128
152
  const pixels: (Pixel | null)[][] = [];
129
- for (let y = 0; y < heightPixels; y++) {
130
- pixels.push(new Array(width).fill(null));
153
+ for (let y = 0; y < bufferHeight; y++) {
154
+ pixels.push(new Array(bufferWidth).fill(null));
131
155
  }
132
- return { width, height: heightPixels, pixels };
156
+ return { width: bufferWidth, height: bufferHeight, pixels };
133
157
  }
134
158
 
135
159
  function setPixel(buf: PixelBuffer, x: number, y: number, color: string, layer: number) {
@@ -413,7 +437,7 @@ function drawOhlcChart(
413
437
  // Volume bars
414
438
  // ---------------------------------------------------------------------------
415
439
 
416
- export function drawVolumeBars(
440
+ function drawVolumeBars(
417
441
  buf: PixelBuffer,
418
442
  points: ProjectedChartPoint[],
419
443
  yTop: number,
@@ -483,7 +507,7 @@ export function drawGridLines(
483
507
  // Braille buffer → styled terminal output
484
508
  // ---------------------------------------------------------------------------
485
509
 
486
- export function bufferToBrailleLines(buf: PixelBuffer, bgColor: string): StyledContent[] {
510
+ export function bufferToBrailleLines(buf: PixelBuffer): StyledContent[] {
487
511
  const lines: StyledContent[] = [];
488
512
  const termCols = Math.ceil(buf.width / 2);
489
513
  const termRows = Math.ceil(buf.height / 4);
@@ -491,8 +515,8 @@ export function bufferToBrailleLines(buf: PixelBuffer, bgColor: string): StyledC
491
515
  for (let row = 0; row < termRows; row++) {
492
516
  const chunks: StyledChunk[] = [];
493
517
  let runChar = "";
494
- let runFg = "";
495
- let runBg = "";
518
+ let runFg: string | undefined;
519
+ let runBg: string | undefined;
496
520
  let runLen = 0;
497
521
 
498
522
  const flushRun = () => {
@@ -529,13 +553,13 @@ export function bufferToBrailleLines(buf: PixelBuffer, bgColor: string): StyledC
529
553
  }
530
554
 
531
555
  let char: string;
532
- let cellFg: string;
533
- let cellBg: string;
556
+ let cellFg: string | undefined;
557
+ let cellBg: string | undefined;
534
558
 
535
559
  if (topLayer < 0) {
536
560
  char = " ";
537
- cellFg = bgColor;
538
- cellBg = bgColor;
561
+ cellFg = undefined;
562
+ cellBg = undefined;
539
563
  } else {
540
564
  // Only show dots from the highest layer — keeps lines thin in mixed
541
565
  // cells (e.g. area chart where line and fill overlap).
@@ -550,7 +574,7 @@ export function bufferToBrailleLines(buf: PixelBuffer, bgColor: string): StyledC
550
574
  }
551
575
 
552
576
  cellFg = topColor;
553
- cellBg = bgColor;
577
+ cellBg = undefined;
554
578
  }
555
579
 
556
580
  if (char === runChar && cellFg === runFg && cellBg === runBg) {
@@ -599,7 +623,7 @@ function formatPercentAxisValue(value: number): string {
599
623
  return `${prefix}${value.toFixed(decimals)}%`;
600
624
  }
601
625
 
602
- export function formatPriceWithCurrency(
626
+ function formatPriceWithCurrency(
603
627
  value: number,
604
628
  currency = "USD",
605
629
  assetCategory?: string,
@@ -722,12 +746,6 @@ export function formatCursorAxisValue(
722
746
 
723
747
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
724
748
 
725
- export function formatDate(date: Date | string | number): string {
726
- const d = date instanceof Date ? date : new Date(date);
727
- if (isNaN(d.getTime())) return "—";
728
- return `${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
729
- }
730
-
731
749
  export function formatDateShort(date: Date | string | number): string {
732
750
  const d = date instanceof Date ? date : new Date(date);
733
751
  if (isNaN(d.getTime())) return "—";
@@ -901,24 +919,25 @@ function writeAxisLabel(axis: string[], start: number, label: string) {
901
919
  }
902
920
 
903
921
  export function buildTimeAxis(dates: Array<Date | string | number>, width: number): string {
904
- if (dates.length === 0 || width <= 0) return "";
922
+ const axisWidth = normalizeCount(width, 0);
923
+ if (dates.length === 0 || axisWidth <= 0) return "";
905
924
 
906
925
  const normalizedDates = dates.map((value) => (value instanceof Date ? value : new Date(value)));
907
926
  const first = normalizedDates[0]!;
908
927
  const last = normalizedDates[normalizedDates.length - 1]!;
909
928
  const rawSpanMs = last.getTime() - first.getTime();
910
929
  const spanMs = Math.max(rawSpanMs, 1);
911
- const roughLabelCount = Math.max(Math.floor(width / 10), 2);
930
+ const roughLabelCount = Math.max(Math.floor(axisWidth / 10), 2);
912
931
  const minGapMs = getMinPositiveGapMs(normalizedDates);
913
932
  const effectiveStepMs = Math.max(spanMs / Math.max(roughLabelCount - 1, 1), minGapMs || 0);
914
933
  const unit = resolveAxisLabelUnit(effectiveStepMs);
915
934
  const allSameTimestamp = rawSpanMs === 0 && minGapMs === 0;
916
- const axis = new Array(width).fill(" ");
935
+ const axis = new Array(axisWidth).fill(" ");
917
936
  const minGap = unit === "year" || unit === "month" ? 2 : 1;
918
937
  const idealLabelWidth = estimateAxisLabelWidth(unit, first, last);
919
938
  const targetLabelCount = Math.min(
920
939
  normalizedDates.length,
921
- Math.max(Math.floor(width / (idealLabelWidth + minGap)), 2),
940
+ Math.max(Math.floor(axisWidth / (idealLabelWidth + minGap)), 2),
922
941
  );
923
942
  const candidateIndices = [...new Set(
924
943
  Array.from({ length: targetLabelCount }, (_, index) => (
@@ -930,33 +949,33 @@ export function buildTimeAxis(dates: Array<Date | string | number>, width: numbe
930
949
 
931
950
  const firstLabel = formatTimeAxisBoundaryLabel(first, unit, last);
932
951
  if (allSameTimestamp) {
933
- const centeredStart = resolveCenteredAxisLabelStart(firstLabel, width);
952
+ const centeredStart = resolveCenteredAxisLabelStart(firstLabel, axisWidth);
934
953
  writeAxisLabel(axis, centeredStart, firstLabel);
935
954
  return axis.join("");
936
955
  }
937
956
 
938
- const firstStart = resolveAxisLabelStart(0, firstLabel, width);
957
+ const firstStart = resolveAxisLabelStart(0, firstLabel, axisWidth);
939
958
  writeAxisLabel(axis, firstStart, firstLabel);
940
959
  const placedLabels = new Set<string>([firstLabel]);
941
960
 
942
961
  let lastPlacedDate = first;
943
962
  let lastEnd = firstStart + firstLabel.length - 1;
944
963
 
945
- const lastPos = width - 1;
964
+ const lastPos = axisWidth - 1;
946
965
  const lastLabel = normalizedDates.length === 1
947
966
  ? firstLabel
948
967
  : formatTimeAxisBoundaryLabel(last, unit, first);
949
- const lastStart = resolveAxisLabelStart(lastPos, lastLabel, width);
968
+ const lastStart = resolveAxisLabelStart(lastPos, lastLabel, axisWidth);
950
969
  const lastFits = normalizedDates.length === 1 || lastStart > lastEnd + minGap;
951
970
 
952
971
  for (const index of candidateIndices.slice(1, -1)) {
953
972
  const date = normalizedDates[index]!;
954
973
  if (isNaN(date.getTime())) continue;
955
974
 
956
- const pos = Math.round((index / (normalizedDates.length - 1)) * (width - 1));
975
+ const pos = Math.round((index / (normalizedDates.length - 1)) * (axisWidth - 1));
957
976
  const label = formatTimeAxisLabel(date, lastPlacedDate, unit);
958
977
  if (placedLabels.has(label)) continue;
959
- const start = resolveAxisLabelStart(pos, label, width);
978
+ const start = resolveAxisLabelStart(pos, label, axisWidth);
960
979
  const end = start + label.length - 1;
961
980
 
962
981
  if (label === axis.slice(start, end + 1).join("")) continue;
@@ -1120,6 +1139,7 @@ export function buildChartScene(
1120
1139
  ): ChartScene | null {
1121
1140
  if (points.length === 0) return null;
1122
1141
 
1142
+ const dimensions = normalizeRenderDimensions(opts);
1123
1143
  const dataMin = opts.mode === "candles" || opts.mode === "ohlc"
1124
1144
  ? Math.min(...points.map((point) => point.low))
1125
1145
  : Math.min(...points.map((point) => point.close));
@@ -1129,43 +1149,42 @@ export function buildChartScene(
1129
1149
  const indicatorValues = getIndicatorOverlayValues(opts.indicators);
1130
1150
  const min = indicatorValues.length > 0 ? Math.min(dataMin, ...indicatorValues) : dataMin;
1131
1151
  const max = indicatorValues.length > 0 ? Math.max(dataMax, ...indicatorValues) : dataMax;
1132
- const activeIdx = getActivePointIndex(points.length, opts.width, opts.cursorX, opts.mode);
1152
+ const activeIdx = getActivePointIndex(points.length, dimensions.width, opts.cursorX, opts.mode);
1133
1153
  const activePoint = points[activeIdx]!;
1134
1154
  const range = max - min || 1;
1135
- const chartRows = opts.height - (opts.showVolume ? opts.volumeHeight : 0);
1136
1155
  const cursorX = opts.cursorX === null
1137
1156
  ? null
1138
- : Math.min(Math.max(opts.cursorX, 0), Math.max(opts.width - 1, 0));
1157
+ : Math.min(Math.max(opts.cursorX, 0), Math.max(dimensions.width - 1, 0));
1139
1158
  const cursorColumn = cursorX === null
1140
1159
  ? null
1141
1160
  : Math.round(cursorX);
1142
1161
  const cursorDotX = cursorX === null
1143
1162
  ? null
1144
- : Math.round((cursorX / Math.max(opts.width - 1, 1)) * Math.max(opts.width * 2 - 1, 0));
1163
+ : Math.round((cursorX / Math.max(dimensions.width - 1, 1)) * Math.max(dimensions.width * 2 - 1, 0));
1145
1164
  const cursorY = cursorX === null
1146
1165
  ? null
1147
1166
  : opts.cursorY !== null
1148
- ? Math.min(Math.max(opts.cursorY, 0), Math.max(chartRows - 1, 0))
1167
+ ? Math.min(Math.max(opts.cursorY, 0), Math.max(dimensions.chartRows - 1, 0))
1149
1168
  : Math.min(
1150
- Math.max(Math.round((1 - (activePoint.close - min) / range) * Math.max(chartRows - 1, 0)), 0),
1151
- Math.max(chartRows - 1, 0),
1169
+ Math.max(Math.round((1 - (activePoint.close - min) / range) * Math.max(dimensions.chartRows - 1, 0)), 0),
1170
+ Math.max(dimensions.chartRows - 1, 0),
1152
1171
  );
1153
1172
  const cursorRow = cursorY === null
1154
1173
  ? null
1155
1174
  : Math.round(cursorY);
1156
1175
  const crosshairPrice = cursorY === null
1157
1176
  ? null
1158
- : max - (cursorY / Math.max(chartRows - 1, 1)) * range;
1177
+ : max - (cursorY / Math.max(dimensions.chartRows - 1, 1)) * range;
1159
1178
 
1160
1179
  const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
1161
1180
 
1162
1181
  return {
1163
1182
  points,
1164
- width: opts.width,
1165
- height: opts.height,
1166
- showVolume: opts.showVolume,
1167
- volumeHeight: opts.volumeHeight,
1168
- chartRows,
1183
+ width: dimensions.width,
1184
+ height: dimensions.height,
1185
+ showVolume: dimensions.showVolume,
1186
+ volumeHeight: dimensions.volumeHeight,
1187
+ chartRows: dimensions.chartRows,
1169
1188
  mode: opts.mode,
1170
1189
  colors: opts.colors,
1171
1190
  indicators: opts.indicators ?? null,
@@ -1178,7 +1197,7 @@ export function buildChartScene(
1178
1197
  dateAtCursor: activePoint.date,
1179
1198
  changeAtCursor: activePoint.close - points[0]!.close,
1180
1199
  changePctAtCursor: points[0]!.close ? ((activePoint.close - points[0]!.close) / points[0]!.close) * 100 : 0,
1181
- timeLabels: buildTimeAxis(timeAxisDates, opts.width),
1200
+ timeLabels: buildTimeAxis(timeAxisDates, dimensions.width),
1182
1201
  cursorX,
1183
1202
  cursorY,
1184
1203
  cursorColumn,
@@ -1246,16 +1265,16 @@ export function renderChart(
1246
1265
  };
1247
1266
  }
1248
1267
 
1249
- const { width, colors: palette, mode } = opts;
1268
+ const { width, height, showVolume, volumeHeight, colors: palette, mode } = scene;
1250
1269
  const axisMode = opts.axisMode ?? "price";
1251
1270
  const currency = opts.currency ?? "USD";
1252
1271
  const assetCategory = opts.assetCategory;
1253
1272
 
1254
1273
  // Braille resolution: 2 dot-columns per terminal column, 4 dot-rows per terminal row
1255
1274
  const dotWidth = width * 2;
1256
- const volTermRows = opts.showVolume ? opts.volumeHeight : 0;
1257
- const chartTermRows = opts.height - volTermRows;
1258
- const totalDotH = opts.height * 4;
1275
+ const volTermRows = showVolume ? volumeHeight : 0;
1276
+ const chartTermRows = height - volTermRows;
1277
+ const totalDotH = height * 4;
1259
1278
  const chartDotBottom = chartTermRows * 4 - 1;
1260
1279
  const volDotTop = chartTermRows * 4;
1261
1280
  const volDotBottom = totalDotH - 1;
@@ -1297,7 +1316,7 @@ export function renderChart(
1297
1316
  break;
1298
1317
  }
1299
1318
 
1300
- if (opts.showVolume && volTermRows > 0) {
1319
+ if (showVolume && volTermRows > 0) {
1301
1320
  drawVolumeBars(
1302
1321
  buf,
1303
1322
  points,
@@ -1318,7 +1337,7 @@ export function renderChart(
1318
1337
  const { activePoint, priceAtCursor, crosshairPrice, dateAtCursor, changeAtCursor, changePctAtCursor } = scene;
1319
1338
 
1320
1339
  if (scene.cursorDotX !== null) {
1321
- drawCrosshair(buf, scene.cursorDotX, 0, opts.showVolume ? volDotBottom : chartDotBottom, palette.crosshairColor);
1340
+ drawCrosshair(buf, scene.cursorDotX, 0, showVolume ? volDotBottom : chartDotBottom, palette.crosshairColor);
1322
1341
  }
1323
1342
 
1324
1343
  const axisLabelsByRow = new Map<number, string>();
@@ -1332,7 +1351,7 @@ export function renderChart(
1332
1351
  const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
1333
1352
 
1334
1353
  return {
1335
- lines: bufferToBrailleLines(buf, palette.bgColor),
1354
+ lines: bufferToBrailleLines(buf),
1336
1355
  axisLabels: [...axisLabelsByRow.entries()].map(([row, label]) => ({ row, label })),
1337
1356
  timeLabels: buildTimeAxis(timeAxisDates, width),
1338
1357
  activePoint,
@@ -1348,47 +1367,3 @@ export function renderChart(
1348
1367
  pixelBuffer: buf,
1349
1368
  };
1350
1369
  }
1351
-
1352
- // ---------------------------------------------------------------------------
1353
- // Pixel buffer → RGBA conversion for Kitty graphics protocol
1354
- // ---------------------------------------------------------------------------
1355
-
1356
- function parseHex(hex: string): [number, number, number] {
1357
- const h = hex.replace("#", "");
1358
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
1359
- }
1360
-
1361
- /**
1362
- * Convert a PixelBuffer to RGBA pixel data for use with drawSuperSampleBuffer.
1363
- * Each pixel in the buffer maps to one RGBA pixel in the output.
1364
- */
1365
- export function pixelBufferToRGBA(buf: PixelBuffer, bgColor: string): Uint8Array {
1366
- const [bgR, bgG, bgB] = parseHex(bgColor);
1367
- const data = new Uint8Array(buf.width * buf.height * 4);
1368
- const colorCache = new Map<string, [number, number, number]>();
1369
-
1370
- for (let y = 0; y < buf.height; y++) {
1371
- for (let x = 0; x < buf.width; x++) {
1372
- const offset = (y * buf.width + x) * 4;
1373
- const pixel = buf.pixels[y]?.[x] ?? null;
1374
- if (pixel) {
1375
- let rgb = colorCache.get(pixel.color);
1376
- if (!rgb) {
1377
- rgb = parseHex(pixel.color);
1378
- colorCache.set(pixel.color, rgb);
1379
- }
1380
- data[offset] = rgb[0];
1381
- data[offset + 1] = rgb[1];
1382
- data[offset + 2] = rgb[2];
1383
- data[offset + 3] = 255;
1384
- } else {
1385
- data[offset] = bgR;
1386
- data[offset + 1] = bgG;
1387
- data[offset + 2] = bgB;
1388
- data[offset + 3] = 255;
1389
- }
1390
- }
1391
- }
1392
-
1393
- return data;
1394
- }
@@ -9,7 +9,7 @@ export interface ChartResolutionSupport {
9
9
 
10
10
  export const TIME_RANGE_ORDER: TimeRange[] = ["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"];
11
11
 
12
- export const CHART_RESOLUTION_ORDER: ChartResolution[] = [
12
+ const CHART_RESOLUTION_ORDER: ChartResolution[] = [
13
13
  "auto",
14
14
  "1m",
15
15
  "5m",
@@ -22,7 +22,7 @@ export const CHART_RESOLUTION_ORDER: ChartResolution[] = [
22
22
  "1mo",
23
23
  ];
24
24
 
25
- export const CHART_RESOLUTION_LABELS: Record<ChartResolution, string> = {
25
+ const CHART_RESOLUTION_LABELS: Record<ChartResolution, string> = {
26
26
  auto: "AUTO",
27
27
  "1m": "1M",
28
28
  "5m": "5M",
@@ -35,7 +35,7 @@ export const CHART_RESOLUTION_LABELS: Record<ChartResolution, string> = {
35
35
  "1mo": "1MO",
36
36
  };
37
37
 
38
- export const RANGE_PRESET_RESOLUTION: Record<TimeRange, ManualChartResolution> = {
38
+ const RANGE_PRESET_RESOLUTION: Record<TimeRange, ManualChartResolution> = {
39
39
  "1D": "1m",
40
40
  "1W": "5m",
41
41
  "1M": "15m",
@@ -46,7 +46,7 @@ export const RANGE_PRESET_RESOLUTION: Record<TimeRange, ManualChartResolution> =
46
46
  "ALL": "1mo",
47
47
  };
48
48
 
49
- export const RANGE_PRELOAD_BUFFER: Record<TimeRange, TimeRange> = {
49
+ const RANGE_PRELOAD_BUFFER: Record<TimeRange, TimeRange> = {
50
50
  "1D": "1W",
51
51
  "1W": "1M",
52
52
  "1M": "3M",
@@ -113,11 +113,11 @@ function compareTimeRange(left: TimeRange, right: TimeRange): number {
113
113
  return getTimeRangeIndex(left) - getTimeRangeIndex(right);
114
114
  }
115
115
 
116
- export function isChartResolution(value: unknown): value is ChartResolution {
116
+ function isChartResolution(value: unknown): value is ChartResolution {
117
117
  return CHART_RESOLUTION_ORDER.includes(value as ChartResolution);
118
118
  }
119
119
 
120
- export function isManualChartResolution(value: unknown): value is ManualChartResolution {
120
+ function isManualChartResolution(value: unknown): value is ManualChartResolution {
121
121
  return value !== "auto" && CHART_RESOLUTION_ORDER.includes(value as ChartResolution);
122
122
  }
123
123
 
@@ -137,7 +137,7 @@ function getManualResolutionOrderIndex(resolution: ManualChartResolution): numbe
137
137
  return CHART_RESOLUTION_ORDER.indexOf(resolution);
138
138
  }
139
139
 
140
- export function getPresetBufferRange(range: TimeRange): TimeRange {
140
+ function getPresetBufferRange(range: TimeRange): TimeRange {
141
141
  return RANGE_PRELOAD_BUFFER[range];
142
142
  }
143
143
 
@@ -151,7 +151,7 @@ export function sortChartResolutions<T extends ChartResolution>(resolutions: rea
151
151
  ));
152
152
  }
153
153
 
154
- export function normalizeManualChartResolutions(resolutions: readonly string[]): ManualChartResolution[] {
154
+ function normalizeManualChartResolutions(resolutions: readonly string[]): ManualChartResolution[] {
155
155
  const seen = new Set<ManualChartResolution>();
156
156
  for (const resolution of resolutions) {
157
157
  if (isManualChartResolution(resolution)) {
@@ -169,7 +169,7 @@ export function intersectChartResolutions(resolutionSets: Array<readonly string[
169
169
  );
170
170
  }
171
171
 
172
- export function sortChartResolutionSupport(support: readonly ChartResolutionSupport[]): ChartResolutionSupport[] {
172
+ function sortChartResolutionSupport(support: readonly ChartResolutionSupport[]): ChartResolutionSupport[] {
173
173
  return [...support].sort((left, right) => (
174
174
  CHART_RESOLUTION_ORDER.indexOf(left.resolution) - CHART_RESOLUTION_ORDER.indexOf(right.resolution)
175
175
  ));
@@ -223,7 +223,7 @@ export function getSupportMaxRange(
223
223
  return normalizeChartResolutionSupport(support).find((entry) => entry.resolution === resolution)?.maxRange ?? null;
224
224
  }
225
225
 
226
- export function minTimeRange(left: TimeRange, right: TimeRange): TimeRange {
226
+ function minTimeRange(left: TimeRange, right: TimeRange): TimeRange {
227
227
  return compareTimeRange(left, right) <= 0 ? left : right;
228
228
  }
229
229
 
@@ -32,12 +32,13 @@ describe("chart-scroll", () => {
32
32
  expect(result.remainder).toBeCloseTo(-0.2);
33
33
  });
34
34
 
35
- test("uses slower pan distances for keyboard and drag input", () => {
35
+ test("keeps keyboard pan small while matching drag to the visible window", () => {
36
36
  expect(getKeyboardPanCellCount(100)).toBe(2);
37
- expect(getDragPanPointDelta(10, 100, 200)).toBe(4);
38
- expect(getDragPanWindowRatio(10, 100)).toBeCloseTo(0.02);
39
- expect(resolveDragPanOffset(20, 10, 100, 200, 40)).toBe(16);
40
- expect(resolveDragPanOffset(2, 10, 100, 200, 40)).toBe(0);
37
+ expect(getDragPanPointDelta(10, 100, 200)).toBe(20);
38
+ expect(getDragPanWindowRatio(10, 100)).toBeCloseTo(0.1);
39
+ expect(resolveDragPanOffset(20, 10, 100, 200, 40)).toBe(40);
40
+ expect(resolveDragPanOffset(20, -10, 100, 200, 40)).toBe(0);
41
+ expect(resolveDragPanOffset(38, 10, 100, 200, 40)).toBe(40);
41
42
  });
42
43
 
43
44
  test("resolves wheel direction, cell delta, remainder, and ratio together", () => {
@@ -5,7 +5,7 @@ export type ChartScrollDirection = "up" | "down" | "left" | "right";
5
5
 
6
6
  const KEYBOARD_PAN_WIDTH_RATIO = 0.02;
7
7
  const SCROLL_PAN_WIDTH_RATIO = 0.005;
8
- const DRAG_PAN_VISIBLE_RATIO = 0.2;
8
+ const DRAG_PAN_VISIBLE_RATIO = 1;
9
9
 
10
10
  interface BufferExpansionViewState {
11
11
  activePreset: TimeRange | null;
@@ -45,7 +45,7 @@ export function resolveDragPanOffset(
45
45
  maxPanOffset: number,
46
46
  ): number {
47
47
  return clamp(
48
- startPanOffset - getDragPanPointDelta(deltaCells, chartWidth, visibleCount),
48
+ startPanOffset + getDragPanPointDelta(deltaCells, chartWidth, visibleCount),
49
49
  0,
50
50
  Math.max(maxPanOffset, 0),
51
51
  );
@@ -10,7 +10,6 @@ export type ComparisonChartRenderMode = "area" | "line";
10
10
 
11
11
  export const TIME_RANGES: TimeRange[] = ["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"];
12
12
  export const CHART_RENDER_MODES: ChartRenderMode[] = ["area", "line", "candles", "ohlc"];
13
- export const CHART_AXIS_MODES: ChartAxisMode[] = ["price", "percent"];
14
13
  export const CHART_RENDERER_PREFERENCES: ChartRendererPreference[] = ["auto", "kitty", "braille"];
15
14
  export const COMPARISON_RENDER_MODES: ComparisonChartRenderMode[] = ["area", "line"];
16
15
 
@@ -1,3 +1,4 @@
1
+
1
2
  const MIN_VISIBLE_POINTS = 2;
2
3
  export const RIGHT_EDGE_ANCHOR_RATIO = 1;
3
4
 
@@ -5,7 +6,7 @@ function clamp(value: number, min: number, max: number): number {
5
6
  return Math.max(min, Math.min(max, value));
6
7
  }
7
8
 
8
- export function getMaxChartZoom(totalPoints: number): number {
9
+ function getMaxChartZoom(totalPoints: number): number {
9
10
  if (totalPoints <= 0) return 1;
10
11
  return Math.max(1, totalPoints / MIN_VISIBLE_POINTS);
11
12
  }
@@ -349,6 +349,43 @@ describe("renderChart", () => {
349
349
  expect(scene?.max).toBe(18);
350
350
  });
351
351
 
352
+ test("normalizes fractional web dimensions before drawing volume bars", () => {
353
+ const projection = projectChartData(chartFixture, 12, "area", false);
354
+ const result = renderChart(projection.points, {
355
+ width: 12.5,
356
+ height: 7.3,
357
+ showVolume: true,
358
+ volumeHeight: 3,
359
+ cursorX: null,
360
+ cursorY: null,
361
+ mode: projection.effectiveMode,
362
+ colors: palette,
363
+ });
364
+
365
+ expect(result.lines).toHaveLength(7);
366
+ expect(result.pixelBuffer?.width).toBe(24);
367
+ expect(result.pixelBuffer?.height).toBe(28);
368
+ expect(result.axisLabels.every((entry) => Number.isInteger(entry.row))).toBe(true);
369
+ });
370
+
371
+ test("keeps terminal chart cells transparent", () => {
372
+ const projection = projectChartData(chartFixture, 12, "area", false);
373
+ const result = renderChart(projection.points, {
374
+ width: 12,
375
+ height: 5,
376
+ showVolume: false,
377
+ volumeHeight: 0,
378
+ cursorX: null,
379
+ cursorY: null,
380
+ mode: projection.effectiveMode,
381
+ colors: palette,
382
+ });
383
+
384
+ const chunks = result.lines.flatMap((line) => line.chunks);
385
+ expect(chunks.some((chunk) => chunk.text.trim().length > 0)).toBe(true);
386
+ expect(chunks.every((chunk) => chunk.bg === undefined)).toBe(true);
387
+ });
388
+
352
389
  test("keeps one decimal on zoomed equity axes even when whole-dollar ticks are distinct", () => {
353
390
  const mediumRangeFixture: PricePoint[] = [
354
391
  { date: new Date("2024-01-02T09:30:00Z"), close: 233.82, volume: 100 },
@@ -6,7 +6,6 @@ import type {
6
6
  ComparisonChartViewState,
7
7
  TimeRange,
8
8
  } from "./chart-types";
9
- import { RANGE_DAYS } from "./chart-types";
10
9
  import { getVisiblePointCount, resolveAnchoredChartZoom } from "./chart-viewport";
11
10
 
12
11
  export interface ComparisonProjectedPoint {
@@ -26,14 +25,14 @@ export interface ComparisonProjectedSeries {
26
25
  points: ComparisonProjectedPoint[];
27
26
  }
28
27
 
29
- export interface ComparisonVisibleWindow {
28
+ interface ComparisonVisibleWindow {
30
29
  dates: Date[];
31
30
  startIdx: number;
32
31
  endIdx: number;
33
32
  totalDates: number;
34
33
  }
35
34
 
36
- export interface ComparisonAxisModeResolution {
35
+ interface ComparisonAxisModeResolution {
37
36
  requestedAxisMode: ChartAxisMode;
38
37
  effectiveAxisMode: ChartAxisMode;
39
38
  warning: string | null;
@@ -64,19 +63,6 @@ function normalizeSeriesPoints(points: PricePoint[]): PricePoint[] {
64
63
  ));
65
64
  }
66
65
 
67
- export function filterComparisonSeriesByTimeRange(
68
- series: ComparisonChartSeries[],
69
- range: TimeRange,
70
- ): ComparisonChartSeries[] {
71
- return series.map((entry) => {
72
- const points = normalizeSeriesPoints(entry.points);
73
- if (range === "ALL" || points.length <= RANGE_DAYS[range]) {
74
- return { ...entry, points };
75
- }
76
- return { ...entry, points: points.slice(-RANGE_DAYS[range]) };
77
- });
78
- }
79
-
80
66
  function getUniqueSortedDates(series: ComparisonChartSeries[]): Date[] {
81
67
  const byTimestamp = new Map<number, Date>();
82
68
 
@@ -185,7 +171,7 @@ function buildSeriesMap(points: PricePoint[]): Map<number, number> {
185
171
  return map;
186
172
  }
187
173
 
188
- export function resolveComparisonAxisMode(
174
+ function resolveComparisonAxisMode(
189
175
  requestedAxisMode: ChartAxisMode,
190
176
  series: ComparisonChartSeries[],
191
177
  visibleDates: Date[],