gloomberb 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,14 +1,17 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { useDialog, useDialogKeyboard, type AlertContext } from "@opentui-ui/dialog/react";
3
- import { useEffect, useRef, useState } from "react";
1
+ /// <reference lib="dom" />
2
+
3
+ import { Box, Text, useUiHost } from "../ui";
4
+ import { TextAttributes } from "../ui";
5
+ import { type AlertContext, useDialog, useDialogKeyboard } from "../ui/dialog";
6
+ import { useEffect, useRef, useState, type ReactNode } from "react";
4
7
  import type {
5
8
  PaneSettingField,
6
9
  PaneSettingTextField,
7
10
  } from "../types/plugin";
8
11
  import type { PluginRegistry } from "../plugins/registry";
9
- import { useAppState } from "../state/app-context";
10
12
  import { colors } from "../theme/colors";
11
13
  import { Button, DialogFrame, ListView, MultiSelectDialogContent, TextField } from "./ui";
14
+ import { NativeSelect, openNativeSelect, type NativeSelectElement } from "./ui/native-select";
12
15
 
13
16
  interface PaneSettingsDialogContentProps extends AlertContext {
14
17
  paneId: string;
@@ -16,6 +19,21 @@ interface PaneSettingsDialogContentProps extends AlertContext {
16
19
  applyFieldValue: (paneId: string, field: PaneSettingField, value: unknown) => Promise<void>;
17
20
  }
18
21
 
22
+ type SelectPaneSettingField = Extract<PaneSettingField, { type: "select" }>;
23
+ type MultiSelectPaneSettingField = Extract<PaneSettingField, { type: "multi-select" | "ordered-multi-select" }>;
24
+
25
+ interface SelectFieldDialogProps extends AlertContext {
26
+ field: SelectPaneSettingField;
27
+ currentValue: unknown;
28
+ onApply: (value: string) => Promise<void>;
29
+ }
30
+
31
+ interface TextFieldDialogProps extends AlertContext {
32
+ field: PaneSettingTextField;
33
+ currentValue: unknown;
34
+ onApply: (value: string) => Promise<void>;
35
+ }
36
+
19
37
  function isSpaceKey(event: { name?: string; sequence?: string }): boolean {
20
38
  return event.name === "space" || event.name === " " || event.sequence === " ";
21
39
  }
@@ -53,20 +71,258 @@ function coerceSelectedValues(value: unknown): string[] {
53
71
  : [];
54
72
  }
55
73
 
56
- function SelectFieldDialog({
74
+ const DESKTOP_TEXT_STYLE = {
75
+ letterSpacing: 0,
76
+ lineHeight: "var(--cell-h)",
77
+ } as const;
78
+
79
+ function desktopText(weight?: number) {
80
+ return weight ? { ...DESKTOP_TEXT_STYLE, fontWeight: weight } : DESKTOP_TEXT_STYLE;
81
+ }
82
+
83
+ function DesktopDialogSurface({
84
+ title,
85
+ subtitle,
57
86
  dismiss,
58
- dialogId,
87
+ children,
88
+ }: {
89
+ title: string;
90
+ subtitle?: string;
91
+ dismiss: () => void;
92
+ children: ReactNode;
93
+ }) {
94
+ return (
95
+ <Box
96
+ width={68}
97
+ maxWidth="calc(100vw - 72px)"
98
+ flexDirection="column"
99
+ style={{
100
+ padding: 12,
101
+ }}
102
+ >
103
+ <Box flexDirection="row" alignItems="flex-start" style={{ marginBottom: 10 }}>
104
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
105
+ <Text fg={colors.textBright} style={desktopText(700)}>{title}</Text>
106
+ {subtitle && (
107
+ <Text fg={colors.textMuted} wrapText style={{ ...desktopText(), marginTop: 3 }}>
108
+ {subtitle}
109
+ </Text>
110
+ )}
111
+ </Box>
112
+ <Box
113
+ width={3}
114
+ height={1}
115
+ alignItems="center"
116
+ justifyContent="center"
117
+ onMouseDown={(event: any) => {
118
+ event.stopPropagation?.();
119
+ event.preventDefault?.();
120
+ dismiss();
121
+ }}
122
+ data-gloom-interactive="true"
123
+ style={{
124
+ borderRadius: 6,
125
+ cursor: "pointer",
126
+ marginLeft: 16,
127
+ color: colors.textMuted,
128
+ }}
129
+ >
130
+ <Text fg={colors.textMuted} style={desktopText(700)}>x</Text>
131
+ </Box>
132
+ </Box>
133
+ {children}
134
+ </Box>
135
+ );
136
+ }
137
+
138
+ function DesktopSwitch({
139
+ checked,
140
+ onChange,
141
+ }: {
142
+ checked: boolean;
143
+ onChange: (checked: boolean) => void;
144
+ }) {
145
+ return (
146
+ <Box
147
+ width="42px"
148
+ height="18px"
149
+ flexDirection="row"
150
+ alignItems="center"
151
+ justifyContent={checked ? "flex-end" : "flex-start"}
152
+ backgroundColor={checked ? colors.selected : "rgba(255, 255, 255, 0.07)"}
153
+ onMouseDown={(event: any) => {
154
+ event.stopPropagation?.();
155
+ event.preventDefault?.();
156
+ onChange(!checked);
157
+ }}
158
+ data-gloom-interactive="true"
159
+ style={{
160
+ border: `1px solid ${checked ? colors.borderFocused : colors.border}`,
161
+ borderRadius: 999,
162
+ boxShadow: checked ? "inset 0 1px 0 rgba(255,255,255,0.10)" : "inset 0 1px 0 rgba(255,255,255,0.05)",
163
+ cursor: "pointer",
164
+ paddingInline: 2,
165
+ }}
166
+ >
167
+ <Box
168
+ width="14px"
169
+ height="14px"
170
+ backgroundColor={checked ? colors.selectedText : colors.textMuted}
171
+ style={{
172
+ borderRadius: 999,
173
+ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
174
+ }}
175
+ />
176
+ </Box>
177
+ );
178
+ }
179
+
180
+ function DesktopValuePill({ value }: { value: string }) {
181
+ return (
182
+ <Box
183
+ flexDirection="row"
184
+ alignItems="center"
185
+ backgroundColor="rgba(255, 255, 255, 0.06)"
186
+ style={{
187
+ border: `1px solid ${colors.border}`,
188
+ borderRadius: 6,
189
+ padding: "1px 7px",
190
+ }}
191
+ >
192
+ <Text fg={colors.textDim} style={desktopText(600)}>{value}</Text>
193
+ </Box>
194
+ );
195
+ }
196
+
197
+ function DesktopSettingsRow({
59
198
  field,
199
+ selected,
200
+ hovered,
60
201
  currentValue,
202
+ onHover,
203
+ onEdit,
204
+ onSelectRef,
61
205
  onApply,
62
- }: AlertContext & {
63
- field: Extract<PaneSettingField, { type: "select" }>;
206
+ }: {
207
+ field: PaneSettingField;
208
+ selected: boolean;
209
+ hovered: boolean;
64
210
  currentValue: unknown;
65
- onApply: (value: string) => Promise<void>;
211
+ onHover: () => void;
212
+ onEdit: () => void;
213
+ onSelectRef: (fieldKey: string, element: NativeSelectElement | null) => void;
214
+ onApply: (field: PaneSettingField, value: unknown) => void;
215
+ }) {
216
+ const isToggle = field.type === "toggle";
217
+ const summary = summarizeValue(field, currentValue);
218
+ const control = field.type === "toggle" ? (
219
+ <DesktopSwitch checked={currentValue === true} onChange={(checked) => onApply(field, checked)} />
220
+ ) : field.type === "select" ? (
221
+ <NativeSelect
222
+ value={typeof currentValue === "string" ? currentValue : ""}
223
+ options={field.options}
224
+ includeUnsetOption
225
+ selectRef={(element) => onSelectRef(field.key, element)}
226
+ onChange={(value) => onApply(field, value)}
227
+ />
228
+ ) : (
229
+ <Box flexDirection="row" alignItems="center" gap={1}>
230
+ <DesktopValuePill value={summary} />
231
+ <Text fg={colors.textMuted} style={desktopText(600)}>Edit</Text>
232
+ </Box>
233
+ );
234
+
235
+ return (
236
+ <Box
237
+ flexDirection="column"
238
+ minHeight={field.description ? undefined : 2}
239
+ backgroundColor={hovered || selected ? "rgba(255, 255, 255, 0.045)" : "transparent"}
240
+ onMouseMove={onHover}
241
+ onMouseDown={field.type === "select" ? undefined : (event: any) => {
242
+ event.stopPropagation?.();
243
+ event.preventDefault?.();
244
+ if (isToggle) onApply(field, currentValue !== true);
245
+ else onEdit();
246
+ }}
247
+ data-gloom-interactive={field.type === "select" ? undefined : "true"}
248
+ style={{
249
+ borderRadius: 6,
250
+ cursor: field.type === "select" ? "default" : "pointer",
251
+ padding: "7px 4px 8px",
252
+ transition: "background-color 100ms ease",
253
+ }}
254
+ >
255
+ <Box flexDirection="row" alignItems="center" width="100%">
256
+ <Box flexDirection="row" flexGrow={1} minWidth={0} style={{ paddingRight: 12 }}>
257
+ <Text fg={colors.text} style={desktopText(650)}>{field.label}</Text>
258
+ </Box>
259
+ {control}
260
+ </Box>
261
+ {field.description && (
262
+ <Text fg={colors.textMuted} wrapText style={desktopText()}>
263
+ {field.description}
264
+ </Text>
265
+ )}
266
+ </Box>
267
+ );
268
+ }
269
+
270
+ function DesktopSettingsList({
271
+ fields,
272
+ selectedIndex,
273
+ hoveredFieldKey,
274
+ settings,
275
+ onHover,
276
+ onEdit,
277
+ onSelectRef,
278
+ onApply,
279
+ }: {
280
+ fields: PaneSettingField[];
281
+ selectedIndex: number;
282
+ hoveredFieldKey: string | null;
283
+ settings: Record<string, unknown>;
284
+ onHover: (field: PaneSettingField, index: number) => void;
285
+ onEdit: (field: PaneSettingField, index: number) => void;
286
+ onSelectRef: (fieldKey: string, element: NativeSelectElement | null) => void;
287
+ onApply: (field: PaneSettingField, value: unknown, index: number) => void;
66
288
  }) {
289
+ return (
290
+ <Box
291
+ flexDirection="column"
292
+ style={{
293
+ marginTop: 2,
294
+ }}
295
+ >
296
+ {fields.map((field, index) => (
297
+ <DesktopSettingsRow
298
+ key={field.key}
299
+ field={field}
300
+ selected={index === selectedIndex && hoveredFieldKey === null}
301
+ hovered={hoveredFieldKey === field.key}
302
+ currentValue={settings[field.key]}
303
+ onHover={() => onHover(field, index)}
304
+ onEdit={() => onEdit(field, index)}
305
+ onSelectRef={onSelectRef}
306
+ onApply={(targetField, value) => onApply(targetField, value, index)}
307
+ />
308
+ ))}
309
+ </Box>
310
+ );
311
+ }
312
+
313
+ function useSelectFieldDialogController({
314
+ dismiss,
315
+ field,
316
+ currentValue,
317
+ onApply,
318
+ }: SelectFieldDialogProps) {
67
319
  const initialIndex = Math.max(0, field.options.findIndex((option) => option.value === currentValue));
68
320
  const [selectedIndex, setSelectedIndex] = useState(initialIndex);
69
321
 
322
+ const applyOption = (value: string) => {
323
+ void onApply(value).then(() => dismiss()).catch(() => {});
324
+ };
325
+
70
326
  useDialogKeyboard((event) => {
71
327
  event.stopPropagation();
72
328
  if (event.name === "up" || event.name === "k") setSelectedIndex((index) => Math.max(0, index - 1));
@@ -75,9 +331,16 @@ function SelectFieldDialog({
75
331
  else if (event.name === "enter" || event.name === "return" || isSpaceKey(event)) {
76
332
  const option = field.options[selectedIndex];
77
333
  if (!option) return;
78
- void onApply(option.value).then(() => dismiss()).catch(() => {});
334
+ applyOption(option.value);
79
335
  }
80
- }, dialogId);
336
+ });
337
+
338
+ return { selectedIndex, setSelectedIndex, applyOption };
339
+ }
340
+
341
+ function TuiSelectFieldDialog(props: SelectFieldDialogProps) {
342
+ const { field } = props;
343
+ const { selectedIndex, setSelectedIndex, applyOption } = useSelectFieldDialogController(props);
81
344
 
82
345
  return (
83
346
  <DialogFrame
@@ -95,24 +358,18 @@ function SelectFieldDialog({
95
358
  showSelectedDescription
96
359
  onSelect={setSelectedIndex}
97
360
  onActivate={(item) => {
98
- void onApply(item.id).then(() => dismiss()).catch(() => {});
361
+ applyOption(item.id);
99
362
  }}
100
363
  />
101
364
  </DialogFrame>
102
365
  );
103
366
  }
104
367
 
105
- function TextFieldDialog({
368
+ function useTextFieldDialogController({
106
369
  dismiss,
107
- dialogId,
108
- field,
109
370
  currentValue,
110
371
  onApply,
111
- }: AlertContext & {
112
- field: PaneSettingTextField;
113
- currentValue: unknown;
114
- onApply: (value: string) => Promise<void>;
115
- }) {
372
+ }: TextFieldDialogProps) {
116
373
  const [value, setValue] = useState(typeof currentValue === "string" ? currentValue : "");
117
374
  const [error, setError] = useState<string | null>(null);
118
375
  const inputRef = useRef<any>(null);
@@ -126,7 +383,7 @@ function TextFieldDialog({
126
383
  event.stopPropagation();
127
384
  dismiss();
128
385
  }
129
- }, dialogId);
386
+ });
130
387
 
131
388
  const submit = async () => {
132
389
  try {
@@ -138,12 +395,56 @@ function TextFieldDialog({
138
395
  }
139
396
  };
140
397
 
398
+ return { value, setValue, error, inputRef, submit };
399
+ }
400
+
401
+ function TextFieldDialog(props: TextFieldDialogProps) {
402
+ return useUiHost().kind === "desktop-web"
403
+ ? <DesktopTextFieldDialog {...props} />
404
+ : <TuiTextFieldDialog {...props} />;
405
+ }
406
+
407
+ function DesktopTextFieldDialog(props: TextFieldDialogProps) {
408
+ const { dismiss, field } = props;
409
+ const { value, setValue, error, inputRef, submit } = useTextFieldDialogController(props);
410
+
411
+ return (
412
+ <DesktopDialogSurface
413
+ title={field.label}
414
+ subtitle={field.description}
415
+ dismiss={dismiss}
416
+ >
417
+ <Box flexDirection="column" gap={1}>
418
+ <TextField
419
+ inputRef={inputRef}
420
+ value={value}
421
+ placeholder={field.placeholder}
422
+ focused
423
+ width={56}
424
+ onChange={setValue}
425
+ onSubmit={() => { void submit(); }}
426
+ />
427
+ {error && (
428
+ <Text fg={colors.negative} wrapText style={desktopText(600)}>{error}</Text>
429
+ )}
430
+ <Box flexDirection="row" justifyContent="flex-end" style={{ marginTop: 12 }}>
431
+ <Button label="Save" variant="primary" onPress={() => { void submit(); }} />
432
+ </Box>
433
+ </Box>
434
+ </DesktopDialogSurface>
435
+ );
436
+ }
437
+
438
+ function TuiTextFieldDialog(props: TextFieldDialogProps) {
439
+ const { dismiss, field } = props;
440
+ const { value, setValue, error, inputRef, submit } = useTextFieldDialogController(props);
441
+
141
442
  return (
142
443
  <DialogFrame
143
444
  title={field.label}
144
445
  footer="Enter to apply · esc cancel"
145
446
  >
146
- <box flexDirection="column" gap={1}>
447
+ <Box flexDirection="column" gap={1}>
147
448
  <TextField
148
449
  inputRef={inputRef}
149
450
  value={value}
@@ -153,20 +454,20 @@ function TextFieldDialog({
153
454
  onSubmit={() => { void submit(); }}
154
455
  />
155
456
  {error && (
156
- <box height={1}>
157
- <text fg={colors.negative}>{error}</text>
158
- </box>
457
+ <Box height={1}>
458
+ <Text fg={colors.negative}>{error}</Text>
459
+ </Box>
159
460
  )}
160
461
  {field.description && (
161
- <box height={1}>
162
- <text fg={colors.textDim}>{field.description}</text>
163
- </box>
462
+ <Box height={1}>
463
+ <Text fg={colors.textDim}>{field.description}</Text>
464
+ </Box>
164
465
  )}
165
- <box flexDirection="row" gap={1}>
466
+ <Box flexDirection="row" gap={1}>
166
467
  <Button label="Apply" variant="primary" onPress={() => { void submit(); }} />
167
468
  <Button label="Cancel" variant="ghost" onPress={dismiss} />
168
- </box>
169
- </box>
469
+ </Box>
470
+ </Box>
170
471
  </DialogFrame>
171
472
  );
172
473
  }
@@ -178,7 +479,7 @@ function MultiSelectFieldDialog({
178
479
  currentValue,
179
480
  onApply,
180
481
  }: AlertContext & {
181
- field: Extract<PaneSettingField, { type: "multi-select" | "ordered-multi-select" }>;
482
+ field: MultiSelectPaneSettingField;
182
483
  currentValue: unknown;
183
484
  onApply: (value: string[]) => Promise<void>;
184
485
  }) {
@@ -195,17 +496,144 @@ function MultiSelectFieldDialog({
195
496
  );
196
497
  }
197
498
 
499
+ function DesktopUnavailablePaneSettingsDialog({ dismiss }: { dismiss: () => void }) {
500
+ return (
501
+ <DesktopDialogSurface title="Pane Settings" dismiss={dismiss}>
502
+ <Text fg={colors.textDim} wrapText style={desktopText()}>
503
+ This pane is no longer configurable.
504
+ </Text>
505
+ </DesktopDialogSurface>
506
+ );
507
+ }
508
+
509
+ function TuiUnavailablePaneSettingsDialog() {
510
+ return (
511
+ <DialogFrame title="Pane Settings" footer="Press esc to cancel">
512
+ <Text fg={colors.textDim}>This pane is no longer configurable.</Text>
513
+ </DialogFrame>
514
+ );
515
+ }
516
+
517
+ function DesktopPaneSettingsDialogBody({
518
+ title,
519
+ dismiss,
520
+ fields,
521
+ selectedIndex,
522
+ hoveredFieldKey,
523
+ settings,
524
+ onHover,
525
+ onEdit,
526
+ onSelectRef,
527
+ onApply,
528
+ }: {
529
+ title: string;
530
+ dismiss: () => void;
531
+ fields: PaneSettingField[];
532
+ selectedIndex: number;
533
+ hoveredFieldKey: string | null;
534
+ settings: Record<string, unknown>;
535
+ onHover: (field: PaneSettingField, index: number) => void;
536
+ onEdit: (field: PaneSettingField, index: number) => void;
537
+ onSelectRef: (fieldKey: string, element: NativeSelectElement | null) => void;
538
+ onApply: (field: PaneSettingField, value: unknown, index: number) => void;
539
+ }) {
540
+ return (
541
+ <DesktopDialogSurface
542
+ title={title}
543
+ dismiss={dismiss}
544
+ >
545
+ {fields.length === 0 ? (
546
+ <Text fg={colors.textDim} wrapText style={desktopText()}>
547
+ No settings available.
548
+ </Text>
549
+ ) : (
550
+ <DesktopSettingsList
551
+ fields={fields}
552
+ selectedIndex={selectedIndex}
553
+ hoveredFieldKey={hoveredFieldKey}
554
+ settings={settings}
555
+ onHover={onHover}
556
+ onEdit={onEdit}
557
+ onSelectRef={onSelectRef}
558
+ onApply={onApply}
559
+ />
560
+ )}
561
+ </DesktopDialogSurface>
562
+ );
563
+ }
564
+
565
+ function TuiPaneSettingsDialogBody({
566
+ title,
567
+ fields,
568
+ selectedIndex,
569
+ settings,
570
+ onSelect,
571
+ onActivate,
572
+ }: {
573
+ title: string;
574
+ fields: PaneSettingField[];
575
+ selectedIndex: number;
576
+ settings: Record<string, unknown>;
577
+ onSelect: (index: number) => void;
578
+ onActivate: (field: PaneSettingField | undefined) => void;
579
+ }) {
580
+ return (
581
+ <DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc cancel">
582
+ <ListView
583
+ items={fields.map((field) => ({
584
+ id: field.key,
585
+ label: field.label,
586
+ description: field.description,
587
+ detail: summarizeValue(field, settings[field.key]),
588
+ }))}
589
+ selectedIndex={selectedIndex}
590
+ bgColor={colors.commandBg}
591
+ showSelectedDescription
592
+ onSelect={onSelect}
593
+ onActivate={(_, index) => {
594
+ onActivate(fields[index]);
595
+ }}
596
+ renderRow={(item, rowState) => (
597
+ <Box flexDirection="row" justifyContent="space-between" width="100%">
598
+ <Box flexDirection="row">
599
+ <Text fg={rowState.selected ? colors.selectedText : colors.textDim}>
600
+ {rowState.selected ? "\u25b8 " : " "}
601
+ </Text>
602
+ <Text
603
+ fg={rowState.selected ? colors.text : colors.textDim}
604
+ attributes={rowState.selected ? TextAttributes.BOLD : 0}
605
+ >
606
+ {item.label}
607
+ </Text>
608
+ </Box>
609
+ <Text fg={rowState.selected ? colors.textMuted : colors.textMuted}>
610
+ {item.detail}
611
+ </Text>
612
+ </Box>
613
+ )}
614
+ />
615
+ {fields.length === 0 && (
616
+ <Box height={1}>
617
+ <Text fg={colors.textDim}>No settings available.</Text>
618
+ </Box>
619
+ )}
620
+ </DialogFrame>
621
+ );
622
+ }
623
+
198
624
  export function PaneSettingsDialogContent({
199
625
  dismiss,
200
- dialogId,
201
626
  paneId,
202
627
  pluginRegistry,
203
628
  applyFieldValue,
204
629
  }: PaneSettingsDialogContentProps) {
205
- const { state } = useAppState();
206
630
  const dialog = useDialog();
631
+ const isDesktop = useUiHost().kind === "desktop-web";
207
632
  const descriptor = pluginRegistry.resolvePaneSettings(paneId);
208
633
  const [selectedIndex, setSelectedIndex] = useState(0);
634
+ const [hoveredFieldKey, setHoveredFieldKey] = useState<string | null>(null);
635
+ const [, setSettingsRevision] = useState(0);
636
+ const desktopSelectRefs = useRef(new Map<string, NativeSelectElement>());
209
637
 
210
638
  const fields = descriptor?.settingsDef.fields ?? [];
211
639
 
@@ -215,23 +643,37 @@ export function PaneSettingsDialogContent({
215
643
  }
216
644
  }, [fields.length, selectedIndex]);
217
645
 
646
+ const applyAndRefresh = async (field: PaneSettingField, value: unknown) => {
647
+ await applyFieldValue(paneId, field, value);
648
+ setSettingsRevision((revision) => revision + 1);
649
+ };
650
+
651
+ const setDesktopSelectRef = (fieldKey: string, element: NativeSelectElement | null) => {
652
+ if (element) desktopSelectRefs.current.set(fieldKey, element);
653
+ else desktopSelectRefs.current.delete(fieldKey);
654
+ };
655
+
218
656
  const openFieldEditor = async (field: PaneSettingField | undefined) => {
219
657
  if (!field || !descriptor) return;
220
658
  const currentValue = descriptor.context.settings[field.key];
221
659
 
222
660
  if (field.type === "toggle") {
223
- await applyFieldValue(paneId, field, currentValue !== true);
661
+ await applyAndRefresh(field, currentValue !== true);
224
662
  return;
225
663
  }
226
664
 
227
665
  if (field.type === "select") {
666
+ if (isDesktop) {
667
+ openNativeSelect(desktopSelectRefs.current.get(field.key));
668
+ return;
669
+ }
228
670
  await dialog.alert({
229
- content: (ctx) => (
230
- <SelectFieldDialog
671
+ content: (ctx: AlertContext) => (
672
+ <TuiSelectFieldDialog
231
673
  {...ctx}
232
674
  field={field}
233
675
  currentValue={currentValue}
234
- onApply={(value) => applyFieldValue(paneId, field, value)}
676
+ onApply={(value) => applyAndRefresh(field, value)}
235
677
  />
236
678
  ),
237
679
  });
@@ -240,12 +682,12 @@ export function PaneSettingsDialogContent({
240
682
 
241
683
  if (field.type === "text") {
242
684
  await dialog.alert({
243
- content: (ctx) => (
685
+ content: (ctx: AlertContext) => (
244
686
  <TextFieldDialog
245
687
  {...ctx}
246
688
  field={field}
247
689
  currentValue={currentValue}
248
- onApply={(value) => applyFieldValue(paneId, field, value)}
690
+ onApply={(value) => applyAndRefresh(field, value)}
249
691
  />
250
692
  ),
251
693
  });
@@ -253,12 +695,12 @@ export function PaneSettingsDialogContent({
253
695
  }
254
696
 
255
697
  await dialog.alert({
256
- content: (ctx) => (
698
+ content: (ctx: AlertContext) => (
257
699
  <MultiSelectFieldDialog
258
700
  {...ctx}
259
701
  field={field}
260
702
  currentValue={currentValue}
261
- onApply={(value) => applyFieldValue(paneId, field, value)}
703
+ onApply={(value) => applyAndRefresh(field, value)}
262
704
  />
263
705
  ),
264
706
  });
@@ -272,58 +714,48 @@ export function PaneSettingsDialogContent({
272
714
  else if (event.name === "enter" || event.name === "return" || isSpaceKey(event)) {
273
715
  void openFieldEditor(fields[selectedIndex]).catch(() => {});
274
716
  }
275
- }, dialogId);
717
+ });
276
718
 
277
719
  if (!descriptor) {
278
- return (
279
- <DialogFrame title="Pane Settings" footer="Press esc to cancel">
280
- <text fg={colors.textDim}>This pane is no longer configurable.</text>
281
- </DialogFrame>
282
- );
720
+ return isDesktop
721
+ ? <DesktopUnavailablePaneSettingsDialog dismiss={dismiss} />
722
+ : <TuiUnavailablePaneSettingsDialog />;
283
723
  }
284
724
 
285
725
  const title = descriptor.settingsDef.title ?? `${descriptor.paneDef.name} Settings`;
286
726
 
287
- return (
288
- <DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc cancel">
289
- <ListView
290
- items={fields.map((field) => ({
291
- id: field.key,
292
- label: field.label,
293
- description: field.description,
294
- detail: summarizeValue(field, descriptor.context.settings[field.key]),
295
- }))}
296
- selectedIndex={selectedIndex}
297
- bgColor={colors.commandBg}
298
- showSelectedDescription
299
- onSelect={setSelectedIndex}
300
- onActivate={(_, index) => {
301
- void openFieldEditor(fields[index]).catch(() => {});
302
- }}
303
- renderRow={(item, rowState) => (
304
- <box flexDirection="row" justifyContent="space-between" width="100%">
305
- <box flexDirection="row">
306
- <text fg={rowState.selected ? colors.selectedText : colors.textDim}>
307
- {rowState.selected ? "\u25b8 " : " "}
308
- </text>
309
- <text
310
- fg={rowState.selected ? colors.text : colors.textDim}
311
- attributes={rowState.selected ? TextAttributes.BOLD : 0}
312
- >
313
- {item.label}
314
- </text>
315
- </box>
316
- <text fg={rowState.selected ? colors.textMuted : colors.textMuted}>
317
- {item.detail}
318
- </text>
319
- </box>
320
- )}
321
- />
322
- {fields.length === 0 && state.focusedPaneId === paneId && (
323
- <box height={1}>
324
- <text fg={colors.textDim}>No settings available.</text>
325
- </box>
326
- )}
327
- </DialogFrame>
727
+ return isDesktop ? (
728
+ <DesktopPaneSettingsDialogBody
729
+ title={title}
730
+ dismiss={dismiss}
731
+ fields={fields}
732
+ selectedIndex={selectedIndex}
733
+ hoveredFieldKey={hoveredFieldKey}
734
+ settings={descriptor.context.settings}
735
+ onHover={(field, index) => {
736
+ setSelectedIndex(index);
737
+ setHoveredFieldKey(field.key);
738
+ }}
739
+ onSelectRef={setDesktopSelectRef}
740
+ onEdit={(field, index) => {
741
+ setSelectedIndex(index);
742
+ void openFieldEditor(field).catch(() => {});
743
+ }}
744
+ onApply={(field, value, index) => {
745
+ setSelectedIndex(index);
746
+ void applyAndRefresh(field, value).catch(() => {});
747
+ }}
748
+ />
749
+ ) : (
750
+ <TuiPaneSettingsDialogBody
751
+ title={title}
752
+ fields={fields}
753
+ selectedIndex={selectedIndex}
754
+ settings={descriptor.context.settings}
755
+ onSelect={setSelectedIndex}
756
+ onActivate={(field) => {
757
+ void openFieldEditor(field).catch(() => {});
758
+ }}
759
+ />
328
760
  );
329
761
  }