gloomberb 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/README.md +91 -11
  2. package/package.json +13 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +318 -158
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +53 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/native/surface-visibility.ts +48 -0
  53. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  54. package/src/components/chart/static-chart-surface.tsx +191 -0
  55. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  56. package/src/components/chart/stock-chart.tsx +452 -191
  57. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  58. package/src/components/command-bar/command-bar.test.tsx +269 -20
  59. package/src/components/command-bar/command-bar.tsx +830 -353
  60. package/src/components/command-bar/command-registry.ts +10 -20
  61. package/src/components/command-bar/helpers.test.ts +1 -1
  62. package/src/components/command-bar/helpers.ts +9 -11
  63. package/src/components/command-bar/pane-template-display.ts +4 -8
  64. package/src/components/command-bar/root-shortcuts.ts +12 -11
  65. package/src/components/command-bar/view-model.test.ts +8 -42
  66. package/src/components/command-bar/view-model.ts +8 -36
  67. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  68. package/src/components/command-bar/workflow-ops.ts +40 -9
  69. package/src/components/command-bar/workflow-types.ts +2 -1
  70. package/src/components/data-table-stack-view.test.tsx +206 -0
  71. package/src/components/data-table-stack-view.tsx +152 -0
  72. package/src/components/data-table-view.test.tsx +188 -0
  73. package/src/components/data-table-view.tsx +279 -0
  74. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  75. package/src/components/feed-data-table-stack-view.tsx +365 -0
  76. package/src/components/index.ts +26 -31
  77. package/src/components/layout/detached-pane-shell.tsx +185 -0
  78. package/src/components/layout/floating-pane.tsx +87 -14
  79. package/src/components/layout/header.tsx +139 -49
  80. package/src/components/layout/pane-content.tsx +42 -0
  81. package/src/components/layout/pane-footer.test.tsx +195 -0
  82. package/src/components/layout/pane-footer.tsx +425 -0
  83. package/src/components/layout/pane-header.test.tsx +39 -0
  84. package/src/components/layout/pane-header.tsx +192 -14
  85. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  86. package/src/components/layout/pane-sizing.ts +9 -10
  87. package/src/components/layout/pane-title.ts +35 -0
  88. package/src/components/layout/pane.tsx +52 -11
  89. package/src/components/layout/shell.test.tsx +315 -107
  90. package/src/components/layout/shell.tsx +1010 -368
  91. package/src/components/layout/status-bar.test.tsx +17 -10
  92. package/src/components/layout/status-bar.tsx +367 -100
  93. package/src/components/layout/titlebar-overlay.ts +2 -0
  94. package/src/components/markdown-editor.tsx +29 -14
  95. package/src/components/markdown-text.tsx +25 -24
  96. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  97. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  98. package/src/components/pane-settings-dialog.tsx +521 -89
  99. package/src/components/pane-template-wizard.test.tsx +1 -1
  100. package/src/components/pane-template-wizard.tsx +38 -37
  101. package/src/components/price-selector-dialog.tsx +22 -21
  102. package/src/components/ticker-badge-list.tsx +53 -0
  103. package/src/components/ticker-badge-text.test.tsx +44 -1
  104. package/src/components/ticker-badge-text.tsx +78 -38
  105. package/src/components/ticker-badge.tsx +28 -7
  106. package/src/components/ticker-list-table-view.tsx +248 -0
  107. package/src/components/ticker-list-table.test.tsx +47 -1
  108. package/src/components/ticker-list-table.tsx +212 -41
  109. package/src/components/toggle-list.tsx +114 -8
  110. package/src/components/ui/button.tsx +24 -16
  111. package/src/components/ui/choice-dialog.tsx +89 -0
  112. package/src/components/ui/data-table.tsx +313 -114
  113. package/src/components/ui/external-link.tsx +52 -13
  114. package/src/components/ui/fields.tsx +44 -30
  115. package/src/components/ui/frame.tsx +24 -57
  116. package/src/components/ui/index.ts +15 -50
  117. package/src/components/ui/list-view.tsx +65 -25
  118. package/src/components/ui/loading.tsx +5 -60
  119. package/src/components/ui/message-composer.test.tsx +81 -0
  120. package/src/components/ui/message-composer.tsx +139 -0
  121. package/src/components/ui/multi-select-dialog.tsx +100 -32
  122. package/src/components/ui/multi-select.ts +112 -0
  123. package/src/components/ui/native-select.tsx +120 -0
  124. package/src/components/ui/page-stack-view.tsx +49 -19
  125. package/src/components/ui/remote-image.tsx +50 -0
  126. package/src/components/ui/row-value-cache.test.ts +17 -0
  127. package/src/components/ui/row-value-cache.ts +41 -0
  128. package/src/components/ui/shortcut-hint.tsx +55 -0
  129. package/src/components/ui/status.tsx +12 -67
  130. package/src/components/ui/table-layout.ts +59 -0
  131. package/src/components/ui/tabs.tsx +249 -27
  132. package/src/components/ui/toggle.tsx +19 -127
  133. package/src/components/ui/ui.test.tsx +266 -53
  134. package/src/components/use-double-click-activation.ts +7 -2
  135. package/src/components/use-external-link-footer.ts +79 -0
  136. package/src/core/app-services.ts +95 -0
  137. package/src/core/state/app-state.ts +886 -0
  138. package/src/{state → core/state}/session-persistence.ts +7 -6
  139. package/src/data/config-layout.ts +225 -0
  140. package/src/data/config-store-node.ts +390 -0
  141. package/src/data/config-store.test.ts +72 -0
  142. package/src/data/config-store.ts +35 -478
  143. package/src/data/sqlite-json.ts +2 -5
  144. package/src/data/sqlite-retry.ts +1 -0
  145. package/src/index.tsx +2 -68
  146. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  147. package/src/market-data/coordinator.test.ts +73 -4
  148. package/src/market-data/coordinator.ts +190 -24
  149. package/src/market-data/hooks.test.tsx +1 -1
  150. package/src/market-data/hooks.tsx +109 -87
  151. package/src/market-data/query-store.ts +3 -3
  152. package/src/market-data/request-types.test.ts +0 -14
  153. package/src/market-data/result-types.ts +1 -0
  154. package/src/news/aggregator.test.ts +267 -0
  155. package/src/news/aggregator.ts +452 -0
  156. package/src/news/hooks.ts +39 -0
  157. package/src/news/ticker-symbols.ts +21 -0
  158. package/src/news/types.ts +77 -0
  159. package/src/notifications/app-notifier.ts +21 -10
  160. package/src/pane-settings.test.ts +0 -2
  161. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  162. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  163. package/src/plugins/builtin/ai/providers.ts +1 -9
  164. package/src/plugins/builtin/ai/runner.ts +41 -5
  165. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  166. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  167. package/src/plugins/builtin/ai/settings.ts +0 -1
  168. package/src/plugins/builtin/ai/utils.ts +1 -71
  169. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  170. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  171. package/src/plugins/builtin/alerts/index.tsx +263 -155
  172. package/src/plugins/builtin/alerts/types.ts +7 -0
  173. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  174. package/src/plugins/builtin/analytics/index.tsx +78 -120
  175. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  176. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  177. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  178. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  179. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  180. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  181. package/src/plugins/builtin/chat-controller.test.ts +129 -0
  182. package/src/plugins/builtin/chat-controller.ts +85 -14
  183. package/src/plugins/builtin/chat.test.tsx +283 -36
  184. package/src/plugins/builtin/chat.tsx +363 -170
  185. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  186. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  187. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  188. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  189. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  190. package/src/plugins/builtin/correlation/index.tsx +51 -46
  191. package/src/plugins/builtin/correlation/settings.ts +1 -2
  192. package/src/plugins/builtin/debug.tsx +99 -70
  193. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  194. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  195. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  196. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  197. package/src/plugins/builtin/earnings/index.tsx +187 -94
  198. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  199. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  200. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  201. package/src/plugins/builtin/econ/index.tsx +236 -421
  202. package/src/plugins/builtin/econ/types.ts +1 -0
  203. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  204. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  205. package/src/plugins/builtin/help.test.tsx +120 -60
  206. package/src/plugins/builtin/help.tsx +266 -82
  207. package/src/plugins/builtin/holders/index.test.ts +45 -0
  208. package/src/plugins/builtin/holders/index.tsx +949 -0
  209. package/src/plugins/builtin/insider/index.tsx +207 -253
  210. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  211. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  212. package/src/plugins/builtin/layout-manager.tsx +12 -40
  213. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  214. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  215. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  216. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  217. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  218. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  219. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  220. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  221. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  222. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  223. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  224. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  225. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  226. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  227. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  228. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  229. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  230. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  231. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  232. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  233. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  234. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  235. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  236. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  237. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  238. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  239. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  240. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  241. package/src/plugins/builtin/news.tsx +98 -25
  242. package/src/plugins/builtin/notes-files.ts +51 -10
  243. package/src/plugins/builtin/notes.tsx +332 -131
  244. package/src/plugins/builtin/options.test.tsx +185 -0
  245. package/src/plugins/builtin/options.tsx +346 -206
  246. package/src/plugins/builtin/plugin-groups.ts +92 -0
  247. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  248. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  249. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  250. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  251. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  252. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  253. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  254. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  255. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  256. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  257. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  258. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  259. package/src/plugins/builtin/research/index.tsx +724 -0
  260. package/src/plugins/builtin/sec.tsx +67 -19
  261. package/src/plugins/builtin/sectors/index.tsx +42 -80
  262. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  263. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  264. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  265. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  266. package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
  267. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  268. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  269. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
  270. package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
  271. package/src/plugins/builtin/ticker-surface.ts +68 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  273. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  274. package/src/plugins/builtin/yahoo.ts +55 -1
  275. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  276. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  277. package/src/plugins/catalog-ui.test.ts +24 -0
  278. package/src/plugins/catalog-ui.ts +44 -0
  279. package/src/plugins/catalog.ts +2 -50
  280. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  281. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  282. package/src/plugins/ibkr/account-cache.ts +15 -120
  283. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  284. package/src/plugins/ibkr/config.test.ts +0 -18
  285. package/src/plugins/ibkr/dialogs.tsx +14 -56
  286. package/src/plugins/ibkr/flex.test.ts +57 -2
  287. package/src/plugins/ibkr/flex.ts +116 -12
  288. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  289. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  290. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  291. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  292. package/src/plugins/ibkr/gateway-types.ts +27 -0
  293. package/src/plugins/ibkr/index.test.tsx +3 -11
  294. package/src/plugins/ibkr/index.tsx +1 -0
  295. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  296. package/src/plugins/ibkr/price-normalization.ts +1 -0
  297. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  298. package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
  299. package/src/plugins/ibkr/trade-tab.tsx +166 -117
  300. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  301. package/src/plugins/pane-manager.test.ts +3 -1
  302. package/src/plugins/pane-manager.ts +93 -19
  303. package/src/plugins/plugin-runtime.test.tsx +176 -74
  304. package/src/plugins/plugin-runtime.tsx +228 -10
  305. package/src/plugins/prediction-markets/cache.ts +49 -0
  306. package/src/plugins/prediction-markets/categories.ts +0 -10
  307. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  308. package/src/plugins/prediction-markets/chart.tsx +21 -20
  309. package/src/plugins/prediction-markets/cli.ts +1 -0
  310. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  311. package/src/plugins/prediction-markets/controller.ts +37 -108
  312. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  313. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  314. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  315. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  316. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  317. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  318. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  319. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  320. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  321. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  322. package/src/plugins/prediction-markets/launch.ts +1 -1
  323. package/src/plugins/prediction-markets/metrics.ts +25 -12
  324. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  325. package/src/plugins/prediction-markets/pane.tsx +180 -82
  326. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  327. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  328. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  329. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  330. package/src/plugins/prediction-markets/settings.ts +0 -5
  331. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  332. package/src/plugins/registry.test.ts +307 -0
  333. package/src/plugins/registry.ts +293 -82
  334. package/src/react/input.ts +61 -0
  335. package/src/react/plugins/plugin-slot.tsx +21 -0
  336. package/src/react/use-raf-callback.ts +44 -0
  337. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  338. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  339. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  340. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  341. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  342. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  343. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  344. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  345. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  346. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  347. package/src/renderers/electrobun/bun/index.ts +1149 -0
  348. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  349. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  350. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  351. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  352. package/src/renderers/electrobun/view/app-services.ts +348 -0
  353. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  354. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  355. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  356. package/src/renderers/electrobun/view/config-host.ts +35 -0
  357. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  358. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  359. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  360. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  361. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  362. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  363. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  364. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  365. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  366. package/src/renderers/electrobun/view/input-host.tsx +112 -0
  367. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  368. package/src/renderers/electrobun/view/key-event.ts +134 -0
  369. package/src/renderers/electrobun/view/main.tsx +104 -0
  370. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  372. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  373. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  374. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  375. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  376. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  377. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  378. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  379. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  380. package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
  381. package/src/renderers/electrobun/view/update-host.ts +38 -0
  382. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  383. package/src/renderers/opentui/dialog-host.tsx +56 -0
  384. package/src/renderers/opentui/host.tsx +140 -0
  385. package/src/renderers/opentui/image-loader.test.ts +25 -0
  386. package/src/renderers/opentui/image-loader.ts +105 -0
  387. package/src/renderers/opentui/image-surface.tsx +266 -0
  388. package/src/renderers/opentui/input-host.tsx +63 -0
  389. package/src/renderers/opentui/start.tsx +71 -0
  390. package/src/renderers/opentui/test-utils.tsx +76 -0
  391. package/src/renderers/opentui/toast-host.tsx +10 -0
  392. package/src/renderers/opentui/ui-host.tsx +98 -0
  393. package/src/sources/gloomberb-cloud.test.ts +288 -17
  394. package/src/sources/gloomberb-cloud.ts +176 -9
  395. package/src/sources/provider-errors.ts +1 -0
  396. package/src/sources/provider-router.test.ts +220 -81
  397. package/src/sources/provider-router.ts +294 -111
  398. package/src/sources/sec-edgar.ts +19 -6
  399. package/src/sources/yahoo-finance.test.ts +0 -5
  400. package/src/sources/yahoo-finance.ts +420 -11
  401. package/src/state/app-activity.ts +7 -7
  402. package/src/state/app-bootstrap.ts +114 -40
  403. package/src/state/app-context-selector.test.tsx +70 -1
  404. package/src/state/app-context.test.ts +71 -3
  405. package/src/state/app-context.test.tsx +14 -7
  406. package/src/state/app-context.tsx +209 -869
  407. package/src/state/config-save-scheduler.ts +35 -0
  408. package/src/state/pane-scroll-registry.tsx +254 -0
  409. package/src/state/persist-scheduler.test.ts +67 -0
  410. package/src/state/persist-scheduler.ts +69 -0
  411. package/src/state/selectors-ui.ts +55 -0
  412. package/src/state/selectors.ts +15 -71
  413. package/src/state/session-persistence.test.ts +1 -1
  414. package/src/state/ticker-refresh-queue.ts +52 -3
  415. package/src/state/use-inline-tickers.ts +18 -16
  416. package/src/state/use-quote-streaming.test.ts +0 -6
  417. package/src/state/use-quote-streaming.test.tsx +1 -1
  418. package/src/state/use-quote-streaming.ts +3 -2
  419. package/src/state/use-session-snapshot.ts +32 -17
  420. package/src/test-support/plugin-runtime.ts +149 -0
  421. package/src/theme/color-utils.ts +1 -0
  422. package/src/theme/colors.ts +16 -0
  423. package/src/types/broker.ts +16 -1
  424. package/src/types/capability-route-source.ts +23 -0
  425. package/src/types/config.test.ts +23 -0
  426. package/src/types/config.ts +145 -10
  427. package/src/types/context-menu.ts +95 -0
  428. package/src/types/data-provider.ts +34 -3
  429. package/src/types/desktop-menu.ts +13 -0
  430. package/src/types/desktop-window.ts +29 -0
  431. package/src/types/financials.ts +123 -2
  432. package/src/types/instrument.ts +1 -0
  433. package/src/types/news-source.ts +9 -0
  434. package/src/types/persistence.ts +1 -0
  435. package/src/types/plugin.ts +32 -8
  436. package/src/ui/ascii-font.ts +85 -0
  437. package/src/ui/context-menu.test.ts +94 -0
  438. package/src/ui/context-menu.tsx +433 -0
  439. package/src/ui/dialog.tsx +55 -0
  440. package/src/ui/host.tsx +364 -0
  441. package/src/ui/index.tsx +134 -0
  442. package/src/ui/toast.tsx +40 -0
  443. package/src/updater.test.ts +69 -0
  444. package/src/updater.ts +61 -18
  445. package/src/utils/api-client.test.ts +99 -9
  446. package/src/utils/api-client.ts +378 -18
  447. package/src/utils/back-navigation.ts +6 -0
  448. package/src/utils/cli-output.ts +1 -0
  449. package/src/utils/currency-units.test.ts +1 -6
  450. package/src/utils/currency-units.ts +1 -0
  451. package/src/utils/debug-log.ts +68 -0
  452. package/src/utils/dom-screenshot.ts +326 -0
  453. package/src/utils/exchange-rate-map.ts +1 -0
  454. package/src/utils/exchanges.ts +214 -0
  455. package/src/utils/format.test.ts +7 -0
  456. package/src/utils/format.ts +1 -0
  457. package/src/utils/fuzzy-search.ts +1 -0
  458. package/src/utils/html-entities.test.ts +8 -0
  459. package/src/utils/html-entities.ts +32 -0
  460. package/src/utils/http-transport.ts +11 -0
  461. package/src/utils/main-thread-monitor.ts +127 -0
  462. package/src/utils/perf-marks.ts +54 -0
  463. package/src/utils/quote-freshness.ts +27 -50
  464. package/src/utils/quote-resolution.test.ts +5 -5
  465. package/src/utils/quote-resolution.ts +0 -1
  466. package/src/utils/quote-time.ts +1 -5
  467. package/src/utils/sec.test.ts +0 -12
  468. package/src/utils/selection-clipboard.ts +14 -6
  469. package/src/utils/shortcut-labels.test.ts +20 -0
  470. package/src/utils/shortcut-labels.ts +67 -0
  471. package/src/utils/slugify.ts +1 -0
  472. package/src/utils/terminal-input-reset.ts +1 -0
  473. package/src/utils/text-wrap.ts +82 -0
  474. package/src/utils/throttled-fetch.test.ts +19 -0
  475. package/src/utils/throttled-fetch.ts +11 -9
  476. package/src/utils/ticker-list.test.ts +1 -7
  477. package/src/utils/ticker-list.ts +1 -0
  478. package/src/utils/timestamp.ts +29 -0
  479. package/src/utils/tweet-text.test.ts +13 -0
  480. package/src/utils/tweet-text.ts +18 -0
  481. package/src/utils/url.ts +11 -0
  482. package/src/version.ts +1 -1
  483. package/src/components/chart/chart-control-hint.tsx +0 -39
  484. package/src/components/detail-feed-view.test.tsx +0 -180
  485. package/src/components/detail-feed-view.tsx +0 -531
  486. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  487. package/src/components/layout/pixel-mouse.test.ts +0 -40
  488. package/src/components/spinner.tsx +0 -2
  489. package/src/components/tab-bar.tsx +0 -17
  490. package/src/components/ui/multi-select-chips.tsx +0 -69
  491. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  492. package/src/plugins/builtin/ask-ai.tsx +0 -9
  493. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  494. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  495. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  496. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  497. package/src/plugins/builtin/fred-settings.ts +0 -10
  498. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  499. package/src/plugins/builtin/options-availability.ts +0 -158
  500. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  501. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  502. package/src/plugins/prediction-markets/status.ts +0 -24
  503. package/src/plugins/prediction-markets/table.tsx +0 -87
  504. package/src/utils/quote-time.test.ts +0 -22
  505. package/src/utils/selection-clipboard.test.ts +0 -43
  506. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -0,0 +1,81 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { act, useRef, useState } from "react";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
+ import { type TextareaRenderable } from "../../ui";
5
+ import { MessageComposer } from "./message-composer";
6
+
7
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
8
+
9
+ afterEach(() => {
10
+ if (testSetup) {
11
+ testSetup.renderer.destroy();
12
+ testSetup = undefined;
13
+ }
14
+ });
15
+
16
+ test("renders a terminal prefix, focuses on click, types, and submits", async () => {
17
+ let focusRequests = 0;
18
+ let submitted = "";
19
+
20
+ function Harness() {
21
+ const [focused, setFocused] = useState(false);
22
+ const inputRef = useRef<TextareaRenderable>(null);
23
+
24
+ return (
25
+ <MessageComposer
26
+ inputRef={inputRef}
27
+ initialValue=""
28
+ focused={focused}
29
+ placeholder="Say something..."
30
+ width={32}
31
+ height={1}
32
+ terminalPrefix=" > "
33
+ onFocusRequest={() => {
34
+ focusRequests += 1;
35
+ setFocused(true);
36
+ }}
37
+ keyBindings={[{ name: "return", action: "submit" }]}
38
+ onSubmit={() => {
39
+ submitted = inputRef.current?.editBuffer.getText() ?? "";
40
+ }}
41
+ />
42
+ );
43
+ }
44
+
45
+ await act(async () => {
46
+ testSetup = await testRender(<Harness />, { width: 32, height: 3 });
47
+ });
48
+
49
+ await act(async () => {
50
+ await testSetup!.renderOnce();
51
+ });
52
+
53
+ const initialLines = testSetup.captureCharFrame().split("\n");
54
+ const inputRow = initialLines.findIndex((line) => line.includes("Say something..."));
55
+ const inputCol = initialLines[inputRow]?.indexOf("Say something...") ?? -1;
56
+
57
+ expect(inputRow).toBeGreaterThanOrEqual(0);
58
+ expect(inputCol).toBeGreaterThanOrEqual(0);
59
+ expect(initialLines[inputRow]).toContain("> Say something...");
60
+
61
+ await act(async () => {
62
+ await testSetup!.mockMouse.click(inputCol + 1, inputRow);
63
+ await testSetup!.renderOnce();
64
+ });
65
+
66
+ expect(focusRequests).toBe(1);
67
+
68
+ await act(async () => {
69
+ await testSetup!.mockInput.typeText("hello");
70
+ await testSetup!.renderOnce();
71
+ });
72
+
73
+ expect(testSetup.captureCharFrame()).toContain("> hello");
74
+
75
+ await act(async () => {
76
+ testSetup!.mockInput.pressEnter();
77
+ await testSetup!.renderOnce();
78
+ });
79
+
80
+ expect(submitted).toBe("hello");
81
+ });
@@ -0,0 +1,139 @@
1
+ import { Box, Text, Textarea, useUiHost } from "../../ui";
2
+ import { type ComponentType, type RefObject } from "react";
3
+ import { type TextareaRenderable } from "../../ui";
4
+ import { colors } from "../../theme/colors";
5
+
6
+ export interface MessageComposerProps {
7
+ inputRef?: RefObject<TextareaRenderable | null>;
8
+ initialValue?: string;
9
+ focused?: boolean;
10
+ placeholder?: string;
11
+ width?: number | string;
12
+ height?: number | string;
13
+ terminalPrefix?: string;
14
+ terminalDivider?: boolean;
15
+ terminalBottomInset?: number;
16
+ onFocusRequest?: () => void;
17
+ onInput?: (value: string) => void;
18
+ onSubmit?: () => void;
19
+ keyBindings?: Array<Record<string, unknown>>;
20
+ wrapText?: boolean;
21
+ }
22
+
23
+ export interface MessageComposerBlockHeightOptions {
24
+ height?: MessageComposerProps["height"];
25
+ nativePaneChrome?: boolean;
26
+ terminalDivider?: boolean;
27
+ terminalBottomInset?: number;
28
+ }
29
+
30
+ export function getMessageComposerBlockHeight({
31
+ height = 1,
32
+ nativePaneChrome = false,
33
+ terminalDivider = true,
34
+ terminalBottomInset = 0,
35
+ }: MessageComposerBlockHeightOptions = {}) {
36
+ const inputHeight = typeof height === "number" ? height : 1;
37
+ if (nativePaneChrome) return inputHeight;
38
+ return inputHeight + (terminalDivider ? 1 : 0) + Math.max(0, Math.floor(terminalBottomInset));
39
+ }
40
+
41
+ export function MessageComposer({
42
+ inputRef,
43
+ initialValue = "",
44
+ focused = false,
45
+ placeholder = "",
46
+ width,
47
+ height = 1,
48
+ terminalPrefix = " > ",
49
+ terminalDivider = true,
50
+ terminalBottomInset = 0,
51
+ onFocusRequest,
52
+ onInput,
53
+ onSubmit,
54
+ keyBindings,
55
+ wrapText = false,
56
+ }: MessageComposerProps) {
57
+ const HostMessageComposer = useUiHost().MessageComposer as ComponentType<MessageComposerProps> | undefined;
58
+ if (HostMessageComposer) {
59
+ return (
60
+ <HostMessageComposer
61
+ inputRef={inputRef}
62
+ initialValue={initialValue}
63
+ focused={focused}
64
+ placeholder={placeholder}
65
+ width={width}
66
+ height={height}
67
+ terminalPrefix={terminalPrefix}
68
+ terminalDivider={terminalDivider}
69
+ terminalBottomInset={terminalBottomInset}
70
+ onFocusRequest={onFocusRequest}
71
+ onInput={onInput}
72
+ onSubmit={onSubmit}
73
+ keyBindings={keyBindings}
74
+ wrapText={wrapText}
75
+ />
76
+ );
77
+ }
78
+
79
+ const prefixWidth = terminalPrefix.length;
80
+ const textWidth = typeof width === "number" ? Math.max(width - prefixWidth, 1) : undefined;
81
+ const bottomInsetHeight = Math.max(0, Math.floor(terminalBottomInset));
82
+ const totalHeight = typeof height === "number"
83
+ ? getMessageComposerBlockHeight({ height, terminalDivider, terminalBottomInset })
84
+ : height;
85
+ const requestFocus = () => {
86
+ onFocusRequest?.();
87
+ inputRef?.current?.focus?.();
88
+ };
89
+ const handleInput = (value: string) => {
90
+ if (!focused) onFocusRequest?.();
91
+ onInput?.(value);
92
+ };
93
+
94
+ return (
95
+ <Box
96
+ flexDirection="column"
97
+ width={width}
98
+ height={totalHeight}
99
+ >
100
+ {terminalDivider && (
101
+ <Box height={1} width={width} overflow="hidden">
102
+ <Text fg={colors.border}>{"-".repeat(typeof width === "number" ? width : 0)}</Text>
103
+ </Box>
104
+ )}
105
+ <Box
106
+ flexDirection="row"
107
+ width={width}
108
+ height={height}
109
+ onMouseDown={requestFocus}
110
+ >
111
+ {terminalPrefix.length > 0 && (
112
+ <Box width={prefixWidth} height={height}>
113
+ <Text fg={colors.textDim}>{terminalPrefix}</Text>
114
+ </Box>
115
+ )}
116
+ <Box width={textWidth ?? "100%"} height={height}>
117
+ <Textarea
118
+ ref={inputRef}
119
+ initialValue={initialValue}
120
+ width={textWidth ?? "100%"}
121
+ height={height}
122
+ focused={focused}
123
+ placeholder={placeholder}
124
+ placeholderColor={colors.textMuted}
125
+ textColor={colors.text}
126
+ backgroundColor={colors.bg}
127
+ focusedBackgroundColor={colors.bg}
128
+ cursorColor={colors.textBright}
129
+ onInput={handleInput}
130
+ keyBindings={keyBindings}
131
+ onSubmit={onSubmit}
132
+ wrapText={wrapText}
133
+ />
134
+ </Box>
135
+ </Box>
136
+ {bottomInsetHeight > 0 && <Box height={bottomInsetHeight} width={width} />}
137
+ </Box>
138
+ );
139
+ }
@@ -1,15 +1,23 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { useKeyboard } from "@opentui/react";
3
- import { useDialog, useDialogKeyboard, type AlertContext } from "@opentui-ui/dialog/react";
1
+ import { Box, Text, useUiHost } from "../../ui";
2
+ import { TextAttributes } from "../../ui";
3
+ import { useShortcut } from "../../react/input";
4
+ import { type AlertContext, useDialog, useDialogKeyboard } from "../../ui/dialog";
4
5
  import { type ReactNode, useEffect, useMemo, useState } from "react";
5
6
  import { colors } from "../../theme/colors";
7
+ import { isDetailBackNavigationKey } from "../../utils/back-navigation";
6
8
  import { ToggleList } from "../toggle-list";
7
9
  import { Button } from "./button";
8
10
  import { DialogFrame } from "./frame";
9
11
  import {
12
+ getMultiSelectDisplayValues,
13
+ mergeMultiSelectDisplayValues,
14
+ moveMultiSelectDisplayValue,
10
15
  moveMultiSelectValue,
11
16
  normalizeMultiSelectValues,
17
+ normalizeOrderedMultiSelectValues,
18
+ orderMultiSelectOptionsForDisplay,
12
19
  summarizeMultiSelectValues,
20
+ toggleOrderedMultiSelectValue,
13
21
  toggleMultiSelectValue,
14
22
  type MultiSelectOption,
15
23
  } from "./multi-select";
@@ -70,6 +78,16 @@ function matchesShortcut(event: { name?: string; sequence?: string }, shortcutKe
70
78
  });
71
79
  }
72
80
 
81
+ function normalizeDialogSelectedValues(
82
+ options: readonly MultiSelectOption[],
83
+ values: readonly string[],
84
+ ordered: boolean,
85
+ ): string[] {
86
+ return ordered
87
+ ? normalizeOrderedMultiSelectValues(options, values)
88
+ : normalizeMultiSelectValues(options, values);
89
+ }
90
+
73
91
  export function MultiSelectDialogContent({
74
92
  dismiss,
75
93
  dialogId,
@@ -80,12 +98,18 @@ export function MultiSelectDialogContent({
80
98
  ordered = false,
81
99
  idPrefix,
82
100
  }: MultiSelectDialogContentProps) {
101
+ const isDesktopWeb = useUiHost().kind === "desktop-web";
83
102
  const optionByValue = useMemo(() => new Map(options.map((option) => [option.value, option])), [options]);
84
- const [selectedValues, setSelectedValues] = useState(() => normalizeMultiSelectValues(options, selectedValuesProp));
103
+ const [selectedValues, setSelectedValues] = useState(() => normalizeDialogSelectedValues(options, selectedValuesProp, ordered));
104
+ const [displayValues, setDisplayValues] = useState(() => getMultiSelectDisplayValues(options, selectedValuesProp, ordered));
85
105
  const knownSelectedValues = selectedValues.filter((value) => optionByValue.has(value));
106
+ const displayOptions = useMemo(
107
+ () => orderMultiSelectOptionsForDisplay(options, displayValues),
108
+ [displayValues, options],
109
+ );
86
110
  const [selectedOptionId, setSelectedOptionId] = useState(options[0]?.value ?? "");
87
- const selectedIndex = Math.max(0, options.findIndex((option) => option.value === selectedOptionId));
88
- const selectedOption = options[selectedIndex];
111
+ const selectedIndex = Math.max(0, displayOptions.findIndex((option) => option.value === selectedOptionId));
112
+ const selectedOption = displayOptions[selectedIndex];
89
113
  const selectedOptionValue = selectedOption?.value ?? "";
90
114
  const selectedValueOrder = knownSelectedValues.indexOf(selectedOptionValue);
91
115
  const canMoveUp = ordered && selectedValueOrder > 0;
@@ -94,15 +118,16 @@ export function MultiSelectDialogContent({
94
118
  && selectedValueOrder < knownSelectedValues.length - 1;
95
119
 
96
120
  useEffect(() => {
97
- setSelectedValues(normalizeMultiSelectValues(options, selectedValuesProp));
98
- }, [options, selectedValuesProp]);
121
+ setSelectedValues((values) => normalizeDialogSelectedValues(options, values, ordered));
122
+ setDisplayValues((values) => mergeMultiSelectDisplayValues(options, values));
123
+ }, [options, ordered]);
99
124
 
100
125
  useEffect(() => {
101
- if (options.some((option) => option.value === selectedOptionId)) return;
102
- setSelectedOptionId(options[0]?.value ?? "");
103
- }, [options, selectedOptionId]);
126
+ if (displayOptions.some((option) => option.value === selectedOptionId)) return;
127
+ setSelectedOptionId(displayOptions[0]?.value ?? "");
128
+ }, [displayOptions, selectedOptionId]);
104
129
 
105
- const toggleItems = options.map((option) => {
130
+ const toggleItems = displayOptions.map((option) => {
106
131
  const order = knownSelectedValues.indexOf(option.value);
107
132
  const orderDescription = ordered && order >= 0
108
133
  ? `Order ${order + 1} of ${knownSelectedValues.length}.`
@@ -111,70 +136,93 @@ export function MultiSelectDialogContent({
111
136
  return {
112
137
  id: option.value,
113
138
  label: option.label,
139
+ disabled: option.disabled,
114
140
  enabled: selectedValues.includes(option.value),
115
141
  description: [option.description, orderDescription].filter((entry): entry is string => !!entry).join(" "),
116
142
  };
117
143
  });
118
- const listHeight = Math.min(12, Math.max(6, toggleItems.length));
144
+ const listHeight = isDesktopWeb
145
+ ? Math.min(12, Math.max(5, displayOptions.length * 1.35))
146
+ : Math.min(12, Math.max(6, toggleItems.length));
119
147
 
120
- const applySelectedValues = async (nextValues: string[]) => {
148
+ const applySelectedValues = async (nextValues: string[], nextDisplayValues = displayValues) => {
121
149
  const previousValues = selectedValues;
150
+ const previousDisplayValues = displayValues;
122
151
  setSelectedValues(nextValues);
152
+ setDisplayValues(nextDisplayValues);
123
153
  try {
124
154
  await onChange(nextValues);
125
155
  } catch (error) {
126
156
  setSelectedValues(previousValues);
157
+ setDisplayValues(previousDisplayValues);
127
158
  throw error;
128
159
  }
129
160
  };
130
161
 
131
162
  const toggleOption = async (option: MultiSelectOption | undefined) => {
132
163
  if (!option || option.disabled) return;
133
- await applySelectedValues(toggleMultiSelectValue(options, selectedValues, option.value));
164
+ await applySelectedValues(ordered
165
+ ? toggleOrderedMultiSelectValue(options, selectedValues, option.value)
166
+ : toggleMultiSelectValue(options, selectedValues, option.value));
134
167
  };
135
168
 
136
169
  const moveOption = async (direction: "up" | "down") => {
137
170
  if (!ordered || !selectedOption) return;
138
- await applySelectedValues(moveMultiSelectValue(options, selectedValues, selectedOption.value, direction));
171
+ await applySelectedValues(
172
+ moveMultiSelectValue(options, selectedValues, selectedOption.value, direction),
173
+ moveMultiSelectDisplayValue(displayValues, knownSelectedValues, selectedOption.value, direction),
174
+ );
139
175
  };
140
176
 
141
177
  useDialogKeyboard((event) => {
142
178
  event.stopPropagation();
143
179
  if (event.name === "up" || event.name === "k") {
144
180
  const nextIndex = Math.max(0, selectedIndex - 1);
145
- setSelectedOptionId(options[nextIndex]?.value ?? selectedOptionId);
181
+ setSelectedOptionId(displayOptions[nextIndex]?.value ?? selectedOptionId);
146
182
  } else if (event.name === "down" || event.name === "j") {
147
- const nextIndex = Math.min(options.length - 1, selectedIndex + 1);
148
- setSelectedOptionId(options[nextIndex]?.value ?? selectedOptionId);
183
+ const nextIndex = Math.min(displayOptions.length - 1, selectedIndex + 1);
184
+ setSelectedOptionId(displayOptions[nextIndex]?.value ?? selectedOptionId);
149
185
  } else if (isSpaceKey(event)) {
150
186
  void toggleOption(selectedOption).catch(() => {});
151
187
  } else if (event.name === "[" && ordered) {
152
188
  void moveOption("up").catch(() => {});
153
189
  } else if (event.name === "]" && ordered) {
154
190
  void moveOption("down").catch(() => {});
155
- } else if (event.name === "enter" || event.name === "return" || event.name === "escape") {
191
+ } else if (event.name === "enter" || event.name === "return" || event.name === "escape" || isDetailBackNavigationKey(event)) {
156
192
  dismiss();
157
193
  }
158
194
  }, dialogId);
159
195
 
160
196
  return (
161
- <DialogFrame title={title}>
162
- <box flexDirection="column" gap={1}>
197
+ <DialogFrame title={title} showTitleDivider={!isDesktopWeb}>
198
+ <Box
199
+ flexDirection="column"
200
+ gap={1}
201
+ style={isDesktopWeb ? { minWidth: 520 } : undefined}
202
+ >
163
203
  <ToggleList
164
204
  items={toggleItems}
165
205
  selectedIdx={selectedIndex}
166
- bgColor={colors.commandBg}
206
+ bgColor={isDesktopWeb ? "transparent" : colors.commandBg}
167
207
  height={listHeight}
168
208
  scrollable
169
209
  showSelectedDescription={false}
170
210
  rowIdPrefix={idPrefix ? `${idPrefix}:option` : undefined}
171
- onSelect={(index) => setSelectedOptionId(options[index]?.value ?? selectedOptionId)}
211
+ rowGap={isDesktopWeb ? 0 : undefined}
212
+ rowHeight={isDesktopWeb ? 1.35 : undefined}
213
+ surface={isDesktopWeb ? "plain" : undefined}
214
+ onSelect={(index) => setSelectedOptionId(displayOptions[index]?.value ?? selectedOptionId)}
172
215
  onToggle={(id) => {
173
216
  setSelectedOptionId(id);
174
217
  void toggleOption(optionByValue.get(id)).catch(() => {});
175
218
  }}
176
219
  />
177
- <box flexDirection="row" gap={1}>
220
+ <Box
221
+ flexDirection="row"
222
+ gap={1}
223
+ justifyContent={isDesktopWeb ? "flex-end" : undefined}
224
+ style={isDesktopWeb ? { paddingTop: 6 } : undefined}
225
+ >
178
226
  {ordered && (
179
227
  <>
180
228
  <Button label="Move Up" variant="ghost" disabled={!canMoveUp} onPress={() => { void moveOption("up").catch(() => {}); }} />
@@ -182,8 +230,8 @@ export function MultiSelectDialogContent({
182
230
  </>
183
231
  )}
184
232
  <Button label="Done" variant="primary" onPress={dismiss} />
185
- </box>
186
- </box>
233
+ </Box>
234
+ </Box>
187
235
  </DialogFrame>
188
236
  );
189
237
  }
@@ -202,6 +250,7 @@ export function MultiSelectDialogButton({
202
250
  shortcutKey,
203
251
  shortcutActive = false,
204
252
  }: MultiSelectDialogButtonProps) {
253
+ const isDesktopWeb = useUiHost().kind === "desktop-web";
205
254
  const dialog = useDialog();
206
255
  const summary = summarizeMultiSelectValues({ options, selectedValues, emptyLabel });
207
256
  const buttonLabel = `${label}: ${summary}`;
@@ -226,7 +275,7 @@ export function MultiSelectDialogButton({
226
275
  }).catch(() => {});
227
276
  };
228
277
 
229
- useKeyboard((event) => {
278
+ useShortcut((event) => {
230
279
  if (!shortcutActive || disabled || !matchesShortcut(event, shortcutKey)) return;
231
280
  openDialog(event);
232
281
  });
@@ -242,8 +291,27 @@ export function MultiSelectDialogButton({
242
291
  });
243
292
  }
244
293
 
294
+ if (isDesktopWeb) {
295
+ return (
296
+ <Box
297
+ id={idPrefix ? `${idPrefix}:button` : undefined}
298
+ height={1}
299
+ flexDirection="row"
300
+ onMouseDown={stopMouseEvent}
301
+ onMouseUp={stopMouseEvent}
302
+ >
303
+ <Button
304
+ label={buttonLabel}
305
+ variant="secondary"
306
+ disabled={disabled}
307
+ onPress={() => openDialog()}
308
+ />
309
+ </Box>
310
+ );
311
+ }
312
+
245
313
  return (
246
- <box
314
+ <Box
247
315
  id={idPrefix ? `${idPrefix}:button` : undefined}
248
316
  height={1}
249
317
  width={buttonText.length}
@@ -252,15 +320,15 @@ export function MultiSelectDialogButton({
252
320
  onMouseDown={stopMouseEvent}
253
321
  onMouseUp={openDialog}
254
322
  >
255
- <text
323
+ <Text
256
324
  fg={disabled ? colors.textMuted : colors.selectedText}
257
325
  attributes={TextAttributes.BOLD}
258
326
  onMouseDown={stopMouseEvent}
259
327
  onMouseUp={openDialog}
260
328
  >
261
329
  {buttonText}
262
- </text>
263
- </box>
330
+ </Text>
331
+ </Box>
264
332
  );
265
333
  }
266
334
 
@@ -1,3 +1,4 @@
1
+
1
2
  export interface MultiSelectOption {
2
3
  value: string;
3
4
  label: string;
@@ -18,6 +19,29 @@ export function normalizeMultiSelectValues(
18
19
  ];
19
20
  }
20
21
 
22
+ export function normalizeOrderedMultiSelectValues(
23
+ options: readonly MultiSelectOption[],
24
+ values: readonly string[],
25
+ ): string[] {
26
+ const knownValues = new Set(options.map((option) => option.value));
27
+ const seen = new Set<string>();
28
+ const nextValues: string[] = [];
29
+
30
+ for (const value of values) {
31
+ if (seen.has(value)) continue;
32
+ seen.add(value);
33
+ if (knownValues.has(value)) nextValues.push(value);
34
+ }
35
+
36
+ for (const value of values) {
37
+ if (!knownValues.has(value) && !nextValues.includes(value)) {
38
+ nextValues.push(value);
39
+ }
40
+ }
41
+
42
+ return nextValues;
43
+ }
44
+
21
45
  export function toggleMultiSelectValue(
22
46
  options: readonly MultiSelectOption[],
23
47
  selectedValues: readonly string[],
@@ -33,6 +57,22 @@ export function toggleMultiSelectValue(
33
57
  return normalizeMultiSelectValues(options, [...nextSelected]);
34
58
  }
35
59
 
60
+ export function toggleOrderedMultiSelectValue(
61
+ options: readonly MultiSelectOption[],
62
+ selectedValues: readonly string[],
63
+ value: string,
64
+ ): string[] {
65
+ const knownValues = new Set(options.map((option) => option.value));
66
+ const normalized = normalizeOrderedMultiSelectValues(options, selectedValues);
67
+ if (normalized.includes(value)) {
68
+ return normalized.filter((entry) => entry !== value);
69
+ }
70
+
71
+ const knownSelected = normalized.filter((entry) => knownValues.has(entry));
72
+ const unknownSelected = normalized.filter((entry) => !knownValues.has(entry));
73
+ return [...knownSelected, value, ...unknownSelected];
74
+ }
75
+
36
76
  export function moveMultiSelectValue(
37
77
  options: readonly MultiSelectOption[],
38
78
  selectedValues: readonly string[],
@@ -58,6 +98,78 @@ export function moveMultiSelectValue(
58
98
  return [...next, ...unknownValues];
59
99
  }
60
100
 
101
+ export function getMultiSelectDisplayValues(
102
+ options: readonly MultiSelectOption[],
103
+ selectedValues: readonly string[],
104
+ ordered: boolean,
105
+ ): string[] {
106
+ const optionValues = options.map((option) => option.value);
107
+ if (!ordered) return optionValues;
108
+
109
+ const selectedQueue = normalizeOrderedMultiSelectValues(options, selectedValues);
110
+ const selectedValueSet = new Set(selectedQueue);
111
+ const displayValues: string[] = [];
112
+
113
+ for (const option of options) {
114
+ if (selectedValueSet.has(option.value)) {
115
+ displayValues.push(selectedQueue.shift() ?? option.value);
116
+ } else {
117
+ displayValues.push(option.value);
118
+ }
119
+ }
120
+
121
+ return displayValues;
122
+ }
123
+
124
+ export function mergeMultiSelectDisplayValues(
125
+ options: readonly MultiSelectOption[],
126
+ displayValues: readonly string[],
127
+ ): string[] {
128
+ const optionValues = options.map((option) => option.value);
129
+ const optionValueSet = new Set(optionValues);
130
+ const nextValues = displayValues.filter((value) => optionValueSet.has(value));
131
+ const nextValueSet = new Set(nextValues);
132
+ return [
133
+ ...nextValues,
134
+ ...optionValues.filter((value) => !nextValueSet.has(value)),
135
+ ];
136
+ }
137
+
138
+ export function orderMultiSelectOptionsForDisplay(
139
+ options: readonly MultiSelectOption[],
140
+ displayValues: readonly string[],
141
+ ): MultiSelectOption[] {
142
+ const optionByValue = new Map(options.map((option) => [option.value, option]));
143
+ return mergeMultiSelectDisplayValues(options, displayValues)
144
+ .map((value) => optionByValue.get(value))
145
+ .filter((option): option is MultiSelectOption => option != null);
146
+ }
147
+
148
+ export function moveMultiSelectDisplayValue(
149
+ displayValues: readonly string[],
150
+ selectedValues: readonly string[],
151
+ selectedOption: string,
152
+ direction: "up" | "down",
153
+ ): string[] {
154
+ const selectedIndex = selectedValues.indexOf(selectedOption);
155
+ if (selectedIndex < 0) return [...displayValues];
156
+
157
+ const targetIndex = direction === "up"
158
+ ? Math.max(0, selectedIndex - 1)
159
+ : Math.min(selectedValues.length - 1, selectedIndex + 1);
160
+ if (targetIndex === selectedIndex) return [...displayValues];
161
+
162
+ const targetOption = selectedValues[targetIndex];
163
+ const sourceDisplayIndex = displayValues.indexOf(selectedOption);
164
+ const targetDisplayIndex = targetOption ? displayValues.indexOf(targetOption) : -1;
165
+ if (sourceDisplayIndex < 0 || targetDisplayIndex < 0) return [...displayValues];
166
+
167
+ const nextDisplayValues = [...displayValues];
168
+ nextDisplayValues[sourceDisplayIndex] = targetOption!;
169
+ nextDisplayValues[targetDisplayIndex] = selectedOption;
170
+ return nextDisplayValues;
171
+ }
172
+
61
173
  export function summarizeMultiSelectValues({
62
174
  options,
63
175
  selectedValues,