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
@@ -0,0 +1,32 @@
1
+ const NAMED_ENTITIES: Record<string, string> = {
2
+ amp: "&",
3
+ apos: "'",
4
+ gt: ">",
5
+ lt: "<",
6
+ nbsp: " ",
7
+ quot: "\"",
8
+ };
9
+
10
+ function decodeCodePoint(codePoint: number, fallback: string): string {
11
+ return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff
12
+ ? String.fromCodePoint(codePoint)
13
+ : fallback;
14
+ }
15
+
16
+ export function decodeHtmlEntities(value: string): string {
17
+ return value.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z][a-z0-9]+);?/gi, (match, entity: string) => {
18
+ const normalized = entity.toLowerCase();
19
+
20
+ if (normalized.startsWith("#x")) {
21
+ const codePoint = Number.parseInt(normalized.slice(2), 16);
22
+ return decodeCodePoint(codePoint, match);
23
+ }
24
+
25
+ if (normalized.startsWith("#")) {
26
+ const codePoint = Number.parseInt(normalized.slice(1), 10);
27
+ return decodeCodePoint(codePoint, match);
28
+ }
29
+
30
+ return NAMED_ENTITIES[normalized] ?? match;
31
+ });
32
+ }
@@ -0,0 +1,11 @@
1
+ export type HttpFetchTransport = (url: string, init?: RequestInit) => Promise<Response>;
2
+
3
+ let httpFetchTransport: HttpFetchTransport | null = null;
4
+
5
+ export function setHttpFetchTransport(transport: HttpFetchTransport | null): void {
6
+ httpFetchTransport = transport;
7
+ }
8
+
9
+ export function httpFetch(url: string, init?: RequestInit): Promise<Response> {
10
+ return (httpFetchTransport ?? globalThis.fetch)(url, init);
11
+ }
@@ -0,0 +1,127 @@
1
+ import { debugLog } from "./debug-log";
2
+
3
+ const SAMPLE_INTERVAL_MS = 50;
4
+ const LAG_WARN_MS = 100;
5
+ const LAG_ERROR_MS = 300;
6
+ const LONG_TASK_WARN_MS = 50;
7
+
8
+ const mainThreadLog = debugLog.createLogger("main-thread");
9
+
10
+ function now(): number {
11
+ const perf = (globalThis as { performance?: { now?: () => number } }).performance;
12
+ return typeof perf?.now === "function" ? perf.now() : Date.now();
13
+ }
14
+
15
+ function summarizeLongTask(entry: unknown): Record<string, unknown> {
16
+ const value = entry as {
17
+ name?: string;
18
+ duration?: number;
19
+ startTime?: number;
20
+ attribution?: Array<Record<string, unknown>>;
21
+ };
22
+ return {
23
+ name: value.name,
24
+ durationMs: Math.round((value.duration ?? 0) * 10) / 10,
25
+ startTimeMs: Math.round((value.startTime ?? 0) * 10) / 10,
26
+ attribution: value.attribution?.map((item) => ({
27
+ name: item.name,
28
+ entryType: item.entryType,
29
+ containerType: item.containerType,
30
+ containerName: item.containerName,
31
+ containerId: item.containerId,
32
+ scriptUrl: item.scriptUrl,
33
+ lineNumber: item.lineNumber,
34
+ columnNumber: item.columnNumber,
35
+ })).slice(0, 3),
36
+ };
37
+ }
38
+
39
+ export function startMainThreadMonitor(
40
+ scope: string,
41
+ options: { mirrorToConsole?: boolean } = {},
42
+ ): () => void {
43
+ let stopped = false;
44
+ let lastTick = now();
45
+ let maxLagMs = 0;
46
+ let sampleCount = 0;
47
+
48
+ mainThreadLog.info("monitor started", {
49
+ scope,
50
+ sampleIntervalMs: SAMPLE_INTERVAL_MS,
51
+ lagWarnMs: LAG_WARN_MS,
52
+ lagErrorMs: LAG_ERROR_MS,
53
+ });
54
+
55
+ const intervalId = setInterval(() => {
56
+ const current = now();
57
+ const elapsedMs = current - lastTick;
58
+ lastTick = current;
59
+ sampleCount += 1;
60
+
61
+ const lagMs = elapsedMs - SAMPLE_INTERVAL_MS;
62
+ if (lagMs <= 0) return;
63
+ maxLagMs = Math.max(maxLagMs, lagMs);
64
+ if (lagMs < LAG_WARN_MS) return;
65
+
66
+ const payload = {
67
+ scope,
68
+ lagMs: Math.round(lagMs * 10) / 10,
69
+ elapsedMs: Math.round(elapsedMs * 10) / 10,
70
+ sampleIntervalMs: SAMPLE_INTERVAL_MS,
71
+ sampleCount,
72
+ maxLagMs: Math.round(maxLagMs * 10) / 10,
73
+ };
74
+ if (lagMs >= LAG_ERROR_MS) {
75
+ mainThreadLog.error("event loop blocked", payload);
76
+ } else {
77
+ mainThreadLog.warn("event loop delayed", payload);
78
+ }
79
+ if (options.mirrorToConsole) {
80
+ console.error(
81
+ `perf main-thread.stall scope=${scope} lag_ms=${payload.lagMs} elapsed_ms=${payload.elapsedMs} sample_interval_ms=${SAMPLE_INTERVAL_MS}`,
82
+ );
83
+ }
84
+ }, SAMPLE_INTERVAL_MS);
85
+
86
+ const ObserverCtor = (globalThis as { PerformanceObserver?: unknown }).PerformanceObserver;
87
+ let observer: { disconnect?: () => void } | null = null;
88
+ if (typeof ObserverCtor === "function") {
89
+ try {
90
+ observer = new (ObserverCtor as new (callback: (list: { getEntries: () => unknown[] }) => void) => {
91
+ observe: (options: { entryTypes: string[] }) => void;
92
+ disconnect: () => void;
93
+ })((list) => {
94
+ for (const entry of list.getEntries()) {
95
+ const durationMs = (entry as { duration?: number }).duration ?? 0;
96
+ if (durationMs < LONG_TASK_WARN_MS) continue;
97
+ const payload = { scope, ...summarizeLongTask(entry) };
98
+ if (durationMs >= LAG_ERROR_MS) {
99
+ mainThreadLog.error("browser long task", payload);
100
+ } else {
101
+ mainThreadLog.warn("browser long task", payload);
102
+ }
103
+ if (options.mirrorToConsole) {
104
+ console.error(
105
+ `perf main-thread.longtask scope=${scope} duration_ms=${Math.round(durationMs * 10) / 10}`,
106
+ );
107
+ }
108
+ }
109
+ });
110
+ observer.observe({ entryTypes: ["longtask"] });
111
+ } catch {
112
+ observer = null;
113
+ }
114
+ }
115
+
116
+ return () => {
117
+ if (stopped) return;
118
+ stopped = true;
119
+ clearInterval(intervalId);
120
+ observer?.disconnect?.();
121
+ mainThreadLog.info("monitor stopped", {
122
+ scope,
123
+ sampleCount,
124
+ maxLagMs: Math.round(maxLagMs * 10) / 10,
125
+ });
126
+ };
127
+ }
@@ -0,0 +1,54 @@
1
+ import { debugLog } from "./debug-log";
2
+
3
+ const PERF_WARN_MS = 50;
4
+ const PERF_ERROR_MS = 200;
5
+
6
+ const perfLog = debugLog.createLogger("perf");
7
+
8
+ function now(): number {
9
+ return typeof performance !== "undefined" && typeof performance.now === "function"
10
+ ? performance.now()
11
+ : Date.now();
12
+ }
13
+
14
+ function logSlowPerfSample(
15
+ name: string,
16
+ durationMs: number,
17
+ metadata?: Record<string, unknown>,
18
+ ): void {
19
+ const payload = {
20
+ durationMs: Math.round(durationMs * 10) / 10,
21
+ ...(metadata ?? {}),
22
+ };
23
+ if (durationMs >= PERF_ERROR_MS) {
24
+ perfLog.error(name, payload);
25
+ } else if (durationMs >= PERF_WARN_MS) {
26
+ perfLog.warn(name, payload);
27
+ }
28
+ }
29
+
30
+ export function measurePerf<T>(
31
+ name: string,
32
+ fn: () => T,
33
+ metadata?: Record<string, unknown>,
34
+ ): T {
35
+ const start = now();
36
+ try {
37
+ return fn();
38
+ } finally {
39
+ logSlowPerfSample(name, now() - start, metadata);
40
+ }
41
+ }
42
+
43
+ export async function measurePerfAsync<T>(
44
+ name: string,
45
+ fn: () => Promise<T>,
46
+ metadata?: Record<string, unknown>,
47
+ ): Promise<T> {
48
+ const start = now();
49
+ try {
50
+ return await fn();
51
+ } finally {
52
+ logSlowPerfSample(name, now() - start, metadata);
53
+ }
54
+ }
@@ -1,58 +1,41 @@
1
1
  import type { MarketState, Quote } from "../types/financials";
2
+ import { canonicalExchange, EXCHANGE_TIME_ZONES } from "./exchanges";
2
3
 
3
4
  const US_EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "BATS"]);
4
- const EXCHANGE_TIME_ZONES: Record<string, string> = {
5
- NASDAQ: "America/New_York",
6
- NYSE: "America/New_York",
7
- ARCA: "America/New_York",
8
- AMEX: "America/New_York",
9
- BATS: "America/New_York",
10
- FWB: "Europe/Berlin",
11
- FWB2: "Europe/Berlin",
12
- XETRA: "Europe/Berlin",
13
- SWX: "Europe/Zurich",
14
- SFB: "Europe/Stockholm",
15
- HKEX: "Asia/Hong_Kong",
16
- JPX: "Asia/Tokyo",
17
- TPEX: "Asia/Taipei",
18
- NSE: "Asia/Kolkata",
19
- BSE: "Asia/Kolkata",
20
- LSE: "Europe/London",
21
- };
22
- const EXCHANGE_ALIASES: Record<string, string> = {
23
- NMS: "NASDAQ",
24
- NGM: "NASDAQ",
25
- NCM: "NASDAQ",
26
- NAS: "NASDAQ",
27
- NYQ: "NYSE",
28
- NYS: "NYSE",
29
- PCX: "AMEX",
30
- ASE: "AMEX",
31
- HKG: "HKEX",
32
- GER: "XETRA",
33
- };
5
+ const exchangeLocalDateFormatters = new Map<string, Intl.DateTimeFormat>();
6
+ const usSessionFormatter = new Intl.DateTimeFormat("en-US", {
7
+ timeZone: "America/New_York",
8
+ weekday: "short",
9
+ hour: "2-digit",
10
+ minute: "2-digit",
11
+ hourCycle: "h23",
12
+ });
34
13
 
35
14
  type UsSessionState = Exclude<MarketState, never>;
36
15
 
37
- function normalizeExchange(exchange?: string): string {
38
- const normalized = (exchange ?? "").trim().toUpperCase();
39
- return EXCHANGE_ALIASES[normalized] ?? normalized;
16
+ function isUsExtendedHoursExchange(exchange?: string): boolean {
17
+ return US_EXTENDED_HOURS_EXCHANGES.has(canonicalExchange(exchange));
40
18
  }
41
19
 
42
- function isUsExtendedHoursExchange(exchange?: string): boolean {
43
- return US_EXTENDED_HOURS_EXCHANGES.has(normalizeExchange(exchange));
20
+ function getExchangeLocalDateFormatter(timeZone: string): Intl.DateTimeFormat {
21
+ let formatter = exchangeLocalDateFormatters.get(timeZone);
22
+ if (!formatter) {
23
+ formatter = new Intl.DateTimeFormat("en-CA", {
24
+ timeZone,
25
+ year: "numeric",
26
+ month: "2-digit",
27
+ day: "2-digit",
28
+ });
29
+ exchangeLocalDateFormatters.set(timeZone, formatter);
30
+ }
31
+ return formatter;
44
32
  }
45
33
 
46
34
  function exchangeLocalDate(exchange: string, timestampMs: number): string | null {
47
- const timeZone = EXCHANGE_TIME_ZONES[normalizeExchange(exchange)];
35
+ const timeZone = EXCHANGE_TIME_ZONES[canonicalExchange(exchange)];
48
36
  if (!timeZone) return null;
49
37
 
50
- const parts = new Intl.DateTimeFormat("en-CA", {
51
- timeZone,
52
- year: "numeric",
53
- month: "2-digit",
54
- day: "2-digit",
55
- }).formatToParts(new Date(timestampMs));
38
+ const parts = getExchangeLocalDateFormatter(timeZone).formatToParts(new Date(timestampMs));
56
39
  const year = parts.find((part) => part.type === "year")?.value;
57
40
  const month = parts.find((part) => part.type === "month")?.value;
58
41
  const day = parts.find((part) => part.type === "day")?.value;
@@ -61,13 +44,7 @@ function exchangeLocalDate(exchange: string, timestampMs: number): string | null
61
44
  }
62
45
 
63
46
  function usSessionState(timestampMs: number): UsSessionState {
64
- const parts = new Intl.DateTimeFormat("en-US", {
65
- timeZone: "America/New_York",
66
- weekday: "short",
67
- hour: "2-digit",
68
- minute: "2-digit",
69
- hourCycle: "h23",
70
- }).formatToParts(new Date(timestampMs));
47
+ const parts = usSessionFormatter.formatToParts(new Date(timestampMs));
71
48
 
72
49
  const weekday = parts.find((part) => part.type === "weekday")?.value ?? "";
73
50
  if (weekday === "Sat" || weekday === "Sun") return "CLOSED";
@@ -97,7 +74,7 @@ export function isQuoteStaleForCurrentSession(quote: Quote | null | undefined, n
97
74
  if (!quote) return false;
98
75
  if (isQuoteMissingActiveSessionPrice(quote)) return true;
99
76
 
100
- const exchange = normalizeExchange(quote.listingExchangeName || quote.exchangeName);
77
+ const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
101
78
  if (!exchange || !Number.isFinite(quote.lastUpdated)) return false;
102
79
 
103
80
  const quoteDate = exchangeLocalDate(exchange, quote.lastUpdated);
@@ -40,7 +40,7 @@ describe("quote-resolution", () => {
40
40
  yahoo: {
41
41
  symbol: "AMD",
42
42
  providerId: "yahoo",
43
- dataSource: "yahoo",
43
+ dataSource: "delayed",
44
44
  price: 99.7,
45
45
  currency: "USD",
46
46
  change: 0.7,
@@ -88,7 +88,7 @@ describe("quote-resolution", () => {
88
88
  yahoo: {
89
89
  symbol: "ELF",
90
90
  providerId: "yahoo",
91
- dataSource: "yahoo",
91
+ dataSource: "delayed",
92
92
  price: 87.5,
93
93
  currency: "USD",
94
94
  change: 0,
@@ -127,7 +127,7 @@ describe("quote-resolution", () => {
127
127
  yahoo: {
128
128
  symbol: "AMD",
129
129
  providerId: "yahoo",
130
- dataSource: "yahoo",
130
+ dataSource: "delayed",
131
131
  price: 221.53,
132
132
  currency: "USD",
133
133
  change: 1.35,
@@ -195,7 +195,7 @@ describe("quote-resolution", () => {
195
195
  yahoo: {
196
196
  symbol: "HY9H",
197
197
  providerId: "yahoo",
198
- dataSource: "yahoo",
198
+ dataSource: "delayed",
199
199
  price: 598,
200
200
  currency: "EUR",
201
201
  change: 6,
@@ -220,7 +220,7 @@ describe("quote-resolution", () => {
220
220
  yahoo: {
221
221
  symbol: "HY9H",
222
222
  providerId: "yahoo",
223
- dataSource: "yahoo",
223
+ dataSource: "delayed",
224
224
  price: 598,
225
225
  currency: "EUR",
226
226
  change: 6,
@@ -96,7 +96,6 @@ function sessionConfidenceRank(confidence?: SessionConfidence): number {
96
96
 
97
97
  function inferQuoteProviderId(quote: Quote | QuoteContribution): string {
98
98
  if (quote.providerId?.trim()) return quote.providerId;
99
- if (quote.dataSource === "yahoo") return "yahoo";
100
99
  return "quote";
101
100
  }
102
101
 
@@ -27,11 +27,7 @@ export function formatQuoteAgeWithSource(
27
27
  const age = formatQuoteAge(quote.lastUpdated, now);
28
28
  if (age === "—") return age;
29
29
 
30
- const prefix = quote.dataSource === "delayed"
31
- ? "◷"
32
- : quote.dataSource === "yahoo"
33
- ? "◌"
34
- : "";
30
+ const prefix = quote.dataSource === "delayed" ? "◷" : "";
35
31
  return prefix ? `${prefix}${age}` : age;
36
32
  }
37
33
 
@@ -20,10 +20,6 @@ function makeTicker(overrides: Partial<TickerRecord["metadata"]>): TickerRecord
20
20
  }
21
21
 
22
22
  describe("isUsEquityTicker", () => {
23
- test("accepts plain US stocks", () => {
24
- expect(isUsEquityTicker(makeTicker({ assetCategory: "STK" }))).toBe(true);
25
- });
26
-
27
23
  test("accepts SMART-routed US stocks with a primary exchange", () => {
28
24
  expect(isUsEquityTicker(makeTicker({
29
25
  exchange: "SMART",
@@ -39,14 +35,6 @@ describe("isUsEquityTicker", () => {
39
35
  }))).toBe(true);
40
36
  });
41
37
 
42
- test("rejects non-US listings", () => {
43
- expect(isUsEquityTicker(makeTicker({
44
- exchange: "HKEX",
45
- currency: "HKD",
46
- assetCategory: "STK",
47
- }))).toBe(false);
48
- });
49
-
50
38
  test("rejects non-equity instruments", () => {
51
39
  expect(isUsEquityTicker(makeTicker({
52
40
  assetCategory: "OPT",
@@ -1,7 +1,15 @@
1
- import type { CliRenderer, KeyEvent } from "@opentui/core";
1
+ import { type NativeRendererHost as CliRenderer } from "../ui";
2
+ import { type KeyEventLike as KeyEvent } from "../react/input";
3
+
4
+ function isDarwinWithBunSpawnSync(): boolean {
5
+ return typeof process !== "undefined"
6
+ && process.platform === "darwin"
7
+ && typeof Bun !== "undefined"
8
+ && typeof Bun.spawnSync === "function";
9
+ }
2
10
 
3
11
  function readTextFromSystemClipboard(): string | null {
4
- if (process.platform !== "darwin") {
12
+ if (!isDarwinWithBunSpawnSync()) {
5
13
  return null;
6
14
  }
7
15
 
@@ -20,7 +28,7 @@ function readTextFromSystemClipboard(): string | null {
20
28
  }
21
29
 
22
30
  function copyTextToSystemClipboard(text: string): boolean {
23
- if (text.length === 0 || process.platform !== "darwin") {
31
+ if (text.length === 0 || !isDarwinWithBunSpawnSync()) {
24
32
  return false;
25
33
  }
26
34
 
@@ -44,13 +52,13 @@ export function copySelectionText(
44
52
  return false;
45
53
  }
46
54
 
47
- return renderer.copyToClipboardOSC52(text) || copyTextToSystemClipboard(text);
55
+ return renderer.copyToClipboardOSC52?.(text) === true || copyTextToSystemClipboard(text);
48
56
  }
49
57
 
50
58
  export function copyActiveSelection(
51
59
  renderer: Pick<CliRenderer, "getSelection" | "copyToClipboardOSC52">,
52
60
  ): boolean {
53
- const text = renderer.getSelection()?.getSelectedText() ?? "";
61
+ const text = renderer.getSelection?.()?.getSelectedText() ?? "";
54
62
  return copySelectionText(renderer, text);
55
63
  }
56
64
 
@@ -80,7 +88,7 @@ export function pasteSystemClipboard(
80
88
  renderer: Pick<CliRenderer, "keyInput">,
81
89
  ): boolean {
82
90
  const text = readTextFromSystemClipboard();
83
- if (!text) {
91
+ if (!text || !renderer.keyInput?.processPaste) {
84
92
  return false;
85
93
  }
86
94
  renderer.keyInput.processPaste(new TextEncoder().encode(text));
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { formatPlatformShortcutLabel, formatPrimaryShortcut, getShortcutDisplayMode } from "./shortcut-labels";
3
+
4
+ describe("shortcut labels", () => {
5
+ test("renders command-or-control shortcuts for the active platform", () => {
6
+ expect(formatPlatformShortcutLabel("Cmd/Ctrl+Shift+D", "darwin")).toBe("Cmd+Shift+D");
7
+ expect(formatPlatformShortcutLabel("Cmd/Ctrl+Shift+D", "linux")).toBe("Ctrl+Shift+D");
8
+ expect(formatPlatformShortcutLabel("CmdOrCtrl+W", "win32")).toBe("Ctrl+W");
9
+ expect(formatPrimaryShortcut(["Shift", "G"], "darwin")).toBe("Cmd+Shift+G");
10
+ expect(formatPrimaryShortcut(",", "linux")).toBe("Ctrl+,");
11
+ });
12
+
13
+ test("renders terminal shortcuts with control even on macOS", () => {
14
+ const mode = getShortcutDisplayMode("opentui");
15
+
16
+ expect(formatPrimaryShortcut("W", "darwin", mode)).toBe("Ctrl+W");
17
+ expect(formatPlatformShortcutLabel("CmdOrCtrl+,", "darwin", mode)).toBe("Ctrl+,");
18
+ expect(getShortcutDisplayMode("desktop-web")).toBe("platform");
19
+ });
20
+ });
@@ -0,0 +1,67 @@
1
+ export type ShortcutPlatform = "darwin" | "win32" | "linux" | "unknown";
2
+ export type ShortcutDisplayMode = "platform" | "terminal";
3
+
4
+ function platformFromProcess(): ShortcutPlatform | null {
5
+ const platform = (globalThis as { process?: { platform?: string } }).process?.platform;
6
+ if (platform === "darwin" || platform === "win32" || platform === "linux") return platform;
7
+ return null;
8
+ }
9
+
10
+ function platformFromNavigator(): ShortcutPlatform | null {
11
+ const navigatorLike = (globalThis as {
12
+ navigator?: {
13
+ platform?: string;
14
+ userAgent?: string;
15
+ userAgentData?: { platform?: string };
16
+ };
17
+ }).navigator;
18
+ const raw = [
19
+ navigatorLike?.userAgentData?.platform,
20
+ navigatorLike?.platform,
21
+ navigatorLike?.userAgent,
22
+ ].filter(Boolean).join(" ").toLowerCase();
23
+
24
+ if (!raw) return null;
25
+ if (/(mac|iphone|ipad|ipod)/.test(raw)) return "darwin";
26
+ if (/win/.test(raw)) return "win32";
27
+ if (/(linux|x11)/.test(raw)) return "linux";
28
+ return null;
29
+ }
30
+
31
+ export function detectShortcutPlatform(): ShortcutPlatform {
32
+ return platformFromProcess() ?? platformFromNavigator() ?? "unknown";
33
+ }
34
+
35
+ export function isMacShortcutPlatform(platform: ShortcutPlatform = detectShortcutPlatform()): boolean {
36
+ return platform === "darwin";
37
+ }
38
+
39
+ export function getShortcutDisplayMode(uiKind: "opentui" | "desktop-web" | undefined): ShortcutDisplayMode {
40
+ return uiKind === "opentui" ? "terminal" : "platform";
41
+ }
42
+
43
+ export function getPrimaryShortcutModifier(
44
+ platform: ShortcutPlatform = detectShortcutPlatform(),
45
+ mode: ShortcutDisplayMode = "platform",
46
+ ): "Cmd" | "Ctrl" {
47
+ if (mode === "terminal") return "Ctrl";
48
+ return isMacShortcutPlatform(platform) ? "Cmd" : "Ctrl";
49
+ }
50
+
51
+ export function formatPrimaryShortcut(
52
+ keys: string | readonly string[],
53
+ platform: ShortcutPlatform = detectShortcutPlatform(),
54
+ mode: ShortcutDisplayMode = "platform",
55
+ ): string {
56
+ const keyParts = typeof keys === "string" ? [keys] : keys;
57
+ return [getPrimaryShortcutModifier(platform, mode), ...keyParts].join("+");
58
+ }
59
+
60
+ export function formatPlatformShortcutLabel(
61
+ label: string,
62
+ platform: ShortcutPlatform = detectShortcutPlatform(),
63
+ mode: ShortcutDisplayMode = "platform",
64
+ ): string {
65
+ const primaryModifier = getPrimaryShortcutModifier(platform, mode);
66
+ return label.replace(/Cmd\/Ctrl|CmdOrCtrl/g, primaryModifier);
67
+ }
@@ -1,3 +1,4 @@
1
+
1
2
  export function slugifyName(name: string, fallbackPrefix: string): string {
2
3
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `${fallbackPrefix}-${Date.now()}`;
3
4
  }
@@ -1,3 +1,4 @@
1
+
1
2
  export const TERMINAL_MOUSE_RESET_SEQUENCE = "\x1B[?1016l\x1B[?1006l\x1B[?1005l\x1B[?1015l\x1B[?1003l\x1B[?1002l\x1B[?1000l";
2
3
 
3
4
  interface TerminalResetStdin {
@@ -0,0 +1,82 @@
1
+ export function truncateWithEllipsis(text: string, width: number): string {
2
+ if (width <= 0) return "";
3
+ if (text.length <= width) return text;
4
+ if (width <= 3) return text.slice(0, width);
5
+ return `${text.slice(0, width - 3)}...`;
6
+ }
7
+
8
+ export function wrapTextLines(
9
+ text: string,
10
+ width: number,
11
+ maxLines = Number.MAX_SAFE_INTEGER,
12
+ ): string[] {
13
+ if (width <= 0) return [];
14
+
15
+ const paragraphs = text
16
+ .split(/\r?\n/)
17
+ .map((paragraph) => paragraph.replace(/\s+/g, " ").trim());
18
+ const lines: string[] = [];
19
+
20
+ const pushLine = (line: string) => {
21
+ if (lines.length >= maxLines) return;
22
+ lines.push(line);
23
+ };
24
+
25
+ for (
26
+ let paragraphIndex = 0;
27
+ paragraphIndex < paragraphs.length;
28
+ paragraphIndex += 1
29
+ ) {
30
+ const paragraph = paragraphs[paragraphIndex]!;
31
+ if (!paragraph) {
32
+ pushLine("");
33
+ continue;
34
+ }
35
+
36
+ let current = "";
37
+ for (const rawWord of paragraph.split(" ")) {
38
+ let word = rawWord;
39
+ while (word.length > width) {
40
+ const available = current ? width - current.length - 1 : width;
41
+ if (available <= 0) {
42
+ pushLine(current);
43
+ current = "";
44
+ continue;
45
+ }
46
+ if (word.length <= available) break;
47
+ const piece = word.slice(0, available);
48
+ word = word.slice(available);
49
+ pushLine(current ? `${current} ${piece}` : piece);
50
+ current = "";
51
+ }
52
+
53
+ if (!current) {
54
+ current = word;
55
+ continue;
56
+ }
57
+
58
+ if (current.length + 1 + word.length <= width) {
59
+ current = `${current} ${word}`;
60
+ } else {
61
+ pushLine(current);
62
+ current = word;
63
+ }
64
+ }
65
+
66
+ if (current) pushLine(current);
67
+ if (paragraphIndex < paragraphs.length - 1) pushLine("");
68
+ if (lines.length >= maxLines) break;
69
+ }
70
+
71
+ if (
72
+ lines.length === maxLines &&
73
+ paragraphs.join(" ").length > lines.join(" ").length
74
+ ) {
75
+ lines[maxLines - 1] = truncateWithEllipsis(
76
+ lines[maxLines - 1] ?? "",
77
+ width,
78
+ );
79
+ }
80
+
81
+ return lines;
82
+ }