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,29 +1,41 @@
1
+ import { Box, Input, ScrollBox, Text, Textarea, useUiCapabilities } from "../../ui";
1
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
3
  import type { RefObject } from "react";
3
- import { TextAttributes, type InputRenderable, type TextareaRenderable } from "@opentui/core";
4
- import { useRenderer, useTerminalDimensions } from "@opentui/react";
5
- import { Spinner } from "../spinner";
6
- import { Button, NumberField, TextField } from "../ui";
4
+ import { TextAttributes, type InputRenderable, type ScrollBoxRenderable, type TextareaRenderable } from "../../ui";
5
+ import { useNativeRenderer } from "../../ui";
6
+ import { useViewport } from "../../react/input";
7
+ import { Button, NumberField, Spinner, TextField } from "../ui";
8
+ import { NativeSelect, openNativeSelect, type NativeSelectElement } from "../ui/native-select";
7
9
  import { ToggleList } from "../toggle-list";
8
10
  import {
9
11
  colors,
10
12
  commandBarBg,
11
13
  commandBarHeadingText,
12
14
  commandBarHoverBg,
15
+ commandBarInputBg,
16
+ commandBarPanelBg,
13
17
  commandBarSelectedBg,
14
18
  commandBarSelectedText,
15
19
  commandBarSubtleText,
16
20
  commandBarText,
17
21
  } from "../../theme/colors";
18
- import { getFocusedCollectionId, useAppState, useFocusedTicker } from "../../state/app-context";
22
+ import {
23
+ getEffectiveThemeId,
24
+ getFocusedCollectionId,
25
+ useAppDispatch,
26
+ useAppSelector,
27
+ useFocusedTicker,
28
+ type AppState,
29
+ } from "../../state/app-context";
19
30
  import { fuzzyFilter } from "../../utils/fuzzy-search";
20
31
  import { commands, getThemeOptions, matchPrefix, type Command } from "./command-registry";
21
- import { applyTheme } from "../../theme/colors";
32
+ import { applyTheme, getCurrentThemeId } from "../../theme/colors";
22
33
  import { exportConfig, importConfig, resetAllData, saveConfig } from "../../data/config-store";
23
34
  import type { DataProvider } from "../../types/data-provider";
24
35
  import type { TickerRepository } from "../../data/ticker-repository";
25
36
  import type { PluginRegistry } from "../../plugins/registry";
26
37
  import type { TickerRecord } from "../../types/ticker";
38
+ import type { Quote } from "../../types/financials";
27
39
  import type {
28
40
  CommandDef,
29
41
  PaneSettingField,
@@ -39,8 +51,10 @@ import {
39
51
  resolveFollowBindingInstance,
40
52
  type LayoutConfig,
41
53
  } from "../../types/config";
42
- import { resolveBrokerConfigFields, type BrokerConfigField } from "../../types/broker";
43
- import { buildIbkrConfigFromValues } from "../../plugins/ibkr/config";
54
+ import {
55
+ buildBrokerProfileConfig,
56
+ validateBrokerProfileValues,
57
+ } from "../../brokers/profile-form";
44
58
  import {
45
59
  addPaneFloating,
46
60
  addPaneToLayout,
@@ -178,7 +192,24 @@ interface ListScreenState {
178
192
 
179
193
  type WorkflowStringValues = Record<string, string>;
180
194
 
195
+ type CommandBarListRow =
196
+ | { kind: "spacer"; id: string }
197
+ | { kind: "heading"; id: string; label: string }
198
+ | { kind: "item"; item: ResultItem; globalIdx: number }
199
+ | { kind: "message"; id: string; label: string; dim?: boolean }
200
+ | { kind: "spinner"; id: string; label: string }
201
+ | { kind: "filler"; id: string };
202
+
181
203
  const commandBarLog = debugLog.createLogger("command-bar");
204
+ const COMMAND_BAR_OVERLAY_Z_INDEX = 2_147_483_646;
205
+ const COMMAND_BAR_PANEL_Z_INDEX = 2_147_483_647;
206
+ const NATIVE_COMMAND_BAR_PADDING_X_PX = 14;
207
+ const NATIVE_COMMAND_BAR_PADDING_Y_PX = 14;
208
+
209
+ function getDefaultConfigBackupPath(): string {
210
+ const home = typeof process !== "undefined" ? process.env.HOME : undefined;
211
+ return `${home || "~"}/gloomberb-config-backup.json`;
212
+ }
182
213
 
183
214
  function getInputRef(
184
215
  store: Record<string, RefObject<InputRenderable | TextareaRenderable | null>>,
@@ -190,10 +221,109 @@ function getInputRef(
190
221
  return store[fieldId]!;
191
222
  }
192
223
 
224
+ function normalizeWorkflowCopy(value?: string): string {
225
+ return (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
226
+ }
227
+
228
+ function getWorkflowFieldDescription(field: CommandBarWorkflowField, active: boolean): string | null {
229
+ const description = field.description?.trim();
230
+ if (!description) return null;
231
+ if (normalizeWorkflowCopy(description) === normalizeWorkflowCopy(field.placeholder)) return null;
232
+ return field.type === "textarea" && active ? `${description} Ctrl+S submits.` : description;
233
+ }
234
+
235
+ function isSetAlertWorkflow(route: CommandBarRoute | null): route is CommandBarWorkflowRoute {
236
+ return route?.kind === "workflow"
237
+ && route.payload.kind === "plugin-command"
238
+ && route.payload.actionId === "set-alert";
239
+ }
240
+
241
+ function normalizeAlertWorkflowSymbol(value: CommandBarFieldValue | undefined): string {
242
+ return coerceFieldString(value).trim().toUpperCase();
243
+ }
244
+
245
+ function formatAlertWorkflowPrice(value: number): string {
246
+ return Number.isFinite(value) ? String(value) : "";
247
+ }
248
+
249
+ function formatAlertWorkflowQuote(quote: Quote): string {
250
+ const price = formatAlertWorkflowPrice(quote.price);
251
+ const exchange = quote.fullExchangeName || quote.exchangeName || quote.listingExchangeName || "";
252
+ const name = quote.name || quote.symbol;
253
+ const source = quote.dataSource === "delayed" ? "delayed" : quote.dataSource || "";
254
+ return [quote.symbol, name, exchange, price, source].filter(Boolean).join(" ");
255
+ }
256
+
257
+ function summarizeAlertWorkflowQuoteError(symbol: string, error: unknown): string {
258
+ if (error instanceof Error && error.message.trim()) return error.message;
259
+ return `No quote found for "${symbol}".`;
260
+ }
261
+
262
+ function updateAlertWorkflowFieldDescriptions(
263
+ fields: CommandBarWorkflowField[],
264
+ descriptions: Partial<Record<"symbol" | "price", string | undefined>>,
265
+ ): CommandBarWorkflowField[] {
266
+ return fields.map((field) => {
267
+ if (field.id !== "symbol" && field.id !== "price") return field;
268
+ const description = descriptions[field.id];
269
+ return field.description === description ? field : { ...field, description };
270
+ });
271
+ }
272
+
273
+ function estimateWorkflowBodyRows(route: CommandBarWorkflowRoute): number {
274
+ const visibleFields = getVisibleWorkflowFields(route.fields, route.values);
275
+ const introRows = (route.subtitle ? 1 : 0)
276
+ + (route.description?.length ?? 0)
277
+ + (route.subtitle || (route.description?.length ?? 0) > 0 ? 1 : 0);
278
+ const fieldRows = visibleFields.reduce((total, field, index) => {
279
+ const active = field.id === route.activeFieldId;
280
+ const controlRows = field.type === "textarea" ? 6 : 1;
281
+ const descriptionRows = getWorkflowFieldDescription(field, active) ? 1 : 0;
282
+ const gapRows = index === visibleFields.length - 1 ? 0 : 1;
283
+ return total + 1 + controlRows + descriptionRows + gapRows;
284
+ }, 0);
285
+ const statusRows = (route.error ? 1 : 0) + (route.pending && route.pendingLabel ? 1 : 0);
286
+ return introRows + fieldRows + statusRows + 1;
287
+ }
288
+
193
289
  function orderListResults(results: ResultItem[]): ResultItem[] {
194
290
  return buildSections(results).flatMap((section) => section.items);
195
291
  }
196
292
 
293
+ function buildListRows(listState: ListScreenState): CommandBarListRow[] {
294
+ const rows: CommandBarListRow[] = [];
295
+ const sections = buildSections(listState.results);
296
+ let globalIdx = 0;
297
+ sections.forEach((section, sectionIndex) => {
298
+ if (sectionIndex > 0) {
299
+ rows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
300
+ }
301
+ rows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
302
+ for (const item of section.items) {
303
+ rows.push({ kind: "item", item, globalIdx });
304
+ globalIdx += 1;
305
+ }
306
+ });
307
+ return rows;
308
+ }
309
+
310
+ function buildNativeListRows(listState: ListScreenState, rows: CommandBarListRow[]): CommandBarListRow[] {
311
+ if (listState.searching && rows.length === 0) {
312
+ return [{ kind: "spinner", id: "searching", label: "Searching…" }];
313
+ }
314
+ if (rows.length === 0) {
315
+ return [{ kind: "message", id: "empty", label: listState.emptyLabel }];
316
+ }
317
+ if (listState.searching) {
318
+ return [...rows, { kind: "spinner", id: "searching", label: "Searching…" }];
319
+ }
320
+ return rows;
321
+ }
322
+
323
+ function clampListIndex(index: number, length: number): number {
324
+ return Math.max(0, Math.min(index, Math.max(0, length - 1)));
325
+ }
326
+
197
327
  function getVisibleMultiSelectPickerOptions(
198
328
  route: Extract<CommandBarRoute, { kind: "picker" }>,
199
329
  ): CommandBarPickerOption[] {
@@ -233,16 +363,68 @@ export function CommandBar({
233
363
  quitApp,
234
364
  onCheckForUpdates,
235
365
  }: CommandBarProps) {
236
- const { state, dispatch } = useAppState();
366
+ const dispatch = useAppDispatch();
367
+ const config = useAppSelector((state) => state.config);
368
+ const paneState = useAppSelector((state) => state.paneState);
369
+ const tickers = useAppSelector((state) => state.tickers);
370
+ const financials = useAppSelector((state) => state.financials);
371
+ const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
372
+ const layoutHistory = useAppSelector((state) => state.layoutHistory);
373
+ const recentTickers = useAppSelector((state) => state.recentTickers);
374
+ const commandBarOpen = useAppSelector((state) => state.commandBarOpen);
375
+ const commandBarQuery = useAppSelector((state) => state.commandBarQuery);
376
+ const commandBarLaunchRequest = useAppSelector((state) => state.commandBarLaunchRequest);
377
+ const updateAvailable = useAppSelector((state) => state.updateAvailable);
378
+ const updateProgress = useAppSelector((state) => state.updateProgress);
379
+ const updateCheckInProgress = useAppSelector((state) => state.updateCheckInProgress);
380
+ const updateNotice = useAppSelector((state) => state.updateNotice);
381
+ const state = useMemo(() => ({
382
+ config,
383
+ paneState,
384
+ tickers,
385
+ financials,
386
+ focusedPaneId,
387
+ layoutHistory,
388
+ recentTickers,
389
+ commandBarOpen,
390
+ commandBarQuery,
391
+ commandBarLaunchRequest,
392
+ updateAvailable,
393
+ updateProgress,
394
+ updateCheckInProgress,
395
+ updateNotice,
396
+ }) as AppState, [
397
+ commandBarLaunchRequest,
398
+ commandBarOpen,
399
+ commandBarQuery,
400
+ config,
401
+ financials,
402
+ focusedPaneId,
403
+ layoutHistory,
404
+ paneState,
405
+ recentTickers,
406
+ tickers,
407
+ updateAvailable,
408
+ updateCheckInProgress,
409
+ updateNotice,
410
+ updateProgress,
411
+ ]);
237
412
  const stateRef = useRef(state);
238
413
  stateRef.current = state;
239
414
  const { symbol: activeTickerSymbol, ticker: activeTickerData, financials: activeFinancials } = useFocusedTicker();
240
- const renderer = useRenderer();
241
- const { width: termWidth, height: termHeight } = useTerminalDimensions();
415
+ const renderer = useNativeRenderer();
416
+ const { width: termWidth, height: termHeight } = useViewport();
417
+ const { nativePaneChrome, cellWidthPx = 8, cellHeightPx = 18 } = useUiCapabilities();
418
+ const availableCommands = useMemo(
419
+ () => nativePaneChrome
420
+ ? commands.filter((command) => command.id !== "cycle-chart-renderer")
421
+ : commands,
422
+ [nativePaneChrome],
423
+ );
242
424
  const [rootQuery, setRootQueryValue] = useState(state.commandBarQuery);
243
425
  const rootQueryRef = useRef(rootQuery);
244
426
  rootQueryRef.current = rootQuery;
245
- const rootModeInfo = resolveCommandBarMode(rootQuery);
427
+ const rootModeInfo = resolveCommandBarMode(rootQuery, availableCommands);
246
428
  const [rootSelectedIdx, setRootSelectedIdx] = useState(0);
247
429
  const [rootHoveredIdx, setRootHoveredIdx] = useState<number | null>(null);
248
430
  const [rootSearching, setRootSearching] = useState(false);
@@ -257,12 +439,16 @@ export function CommandBar({
257
439
  const rootSearchRequestIdRef = useRef(0);
258
440
  const rootLastSearchedQueryRef = useRef<string | null>(null);
259
441
  const tickerSearchCacheRef = useRef<Map<string, TickerSearchCandidate[]>>(new Map());
442
+ const alertWorkflowQuoteRequestRef = useRef(0);
260
443
  const skipTickerSearchDebounceRef = useRef(false);
261
444
  const lastMainBrowseRef = useRef<CommandBarMainSnapshot>({ query: "", selectedIdx: 0 });
262
445
  const previousRootSelectionContextRef = useRef<{ query: string; mode: string } | null>(null);
263
446
  const previousRootModeRef = useRef(rootModeInfo.kind);
264
447
  const rootThemeBaseIdRef = useRef<string | null>(null);
265
448
  const workflowInputRefs = useRef<Record<string, RefObject<InputRenderable | TextareaRenderable | null>>>({});
449
+ const workflowNativeSelectRefs = useRef(new Map<string, NativeSelectElement>());
450
+ const workflowScrollRef = useRef<ScrollBoxRenderable | null>(null);
451
+ const nativeListScrollRef = useRef<ScrollBoxRenderable | null>(null);
266
452
  const visibleListStateRef = useRef<ListScreenState | null>(null);
267
453
  const processedLaunchSequenceRef = useRef<number | null>(null);
268
454
  const currentRoute = routeStack[routeStack.length - 1] ?? null;
@@ -271,21 +457,24 @@ export function CommandBar({
271
457
  const activeCollectionId = getFocusedCollectionId(state);
272
458
  const activePortfolio = state.config.portfolios.find((portfolio) => portfolio.id === activeCollectionId);
273
459
 
460
+ const clearThemePreview = useCallback((themeId: string | null | undefined) => {
461
+ if (!themeId) return;
462
+ if (getCurrentThemeId() !== themeId) {
463
+ applyTheme(themeId);
464
+ }
465
+ dispatch({ type: "PREVIEW_THEME", theme: null });
466
+ }, [dispatch]);
467
+
274
468
  const restoreThemePreview = useCallback(() => {
275
469
  const themeRoute = [...routeStack].reverse().find((route) => (
276
470
  route.kind === "mode" && route.screen === "themes"
277
471
  ));
278
472
  if (themeRoute?.kind === "mode" && themeRoute.themeBaseId) {
279
- applyTheme(themeRoute.themeBaseId);
280
- dispatch({ type: "SET_THEME", theme: themeRoute.themeBaseId });
473
+ clearThemePreview(themeRoute.themeBaseId);
281
474
  return;
282
475
  }
283
- const rootThemeBaseId = rootThemeBaseIdRef.current;
284
- if (rootThemeBaseId && stateRef.current.config.theme !== rootThemeBaseId) {
285
- applyTheme(rootThemeBaseId);
286
- dispatch({ type: "SET_THEME", theme: rootThemeBaseId });
287
- }
288
- }, [dispatch, routeStack]);
476
+ clearThemePreview(rootThemeBaseIdRef.current);
477
+ }, [clearThemePreview, routeStack]);
289
478
 
290
479
  const closeAll = useCallback((options?: { revertThemePreview?: boolean }) => {
291
480
  if (options?.revertThemePreview !== false) {
@@ -318,7 +507,10 @@ export function CommandBar({
318
507
  setRouteStack((current) => {
319
508
  if (current.length === 0) return current;
320
509
  const next = [...current];
321
- next[next.length - 1] = updater(next[next.length - 1]!);
510
+ const top = next[next.length - 1]!;
511
+ const updated = updater(top);
512
+ if (updated === top) return current;
513
+ next[next.length - 1] = updated;
322
514
  return next;
323
515
  });
324
516
  }, []);
@@ -330,8 +522,7 @@ export function CommandBar({
330
522
  }
331
523
 
332
524
  if (currentRoute.kind === "mode" && currentRoute.screen === "themes" && currentRoute.themeBaseId) {
333
- applyTheme(currentRoute.themeBaseId);
334
- dispatch({ type: "SET_THEME", theme: currentRoute.themeBaseId });
525
+ clearThemePreview(currentRoute.themeBaseId);
335
526
  }
336
527
 
337
528
  if (routeStack.length === 1 && currentRoute.restoreMain) {
@@ -341,7 +532,7 @@ export function CommandBar({
341
532
  }
342
533
 
343
534
  setRouteStack((current) => current.slice(0, -1));
344
- }, [closeAll, currentRoute, dispatch, routeStack.length, setRootQuery]);
535
+ }, [clearThemePreview, closeAll, currentRoute, routeStack.length, setRootQuery]);
345
536
 
346
537
  const dismissCommandBar = useCallback(() => {
347
538
  if (currentRoute) {
@@ -390,9 +581,24 @@ export function CommandBar({
390
581
  }, [dispatch]);
391
582
 
392
583
  const openFixedTickerPane = useCallback((symbol: string) => {
393
- pluginRegistry.pinTickerFn(symbol, { floating: true, paneType: "ticker-detail" });
584
+ pluginRegistry.pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
394
585
  }, [pluginRegistry]);
395
586
 
587
+ const previewThemeId = useCallback((themeId: string | undefined) => {
588
+ if (!themeId) return;
589
+ if (getCurrentThemeId() !== themeId) {
590
+ applyTheme(themeId);
591
+ }
592
+ const previewTheme = themeId === stateRef.current.config.theme ? null : themeId;
593
+ if (stateRef.current.themePreview !== previewTheme) {
594
+ dispatch({ type: "PREVIEW_THEME", theme: previewTheme });
595
+ }
596
+ }, [dispatch]);
597
+
598
+ const previewThemeSelection = useCallback((listState: ListScreenState, selectedIdx: number) => {
599
+ previewThemeId(listState.results[selectedIdx]?.themeId);
600
+ }, [previewThemeId]);
601
+
396
602
  const focusTicker = useCallback((symbol: string, options?: { forceNewPane?: boolean }) => {
397
603
  const currentState = stateRef.current;
398
604
  const focusedPane = currentState.focusedPaneId
@@ -444,7 +650,7 @@ export function CommandBar({
444
650
  }, [dispatch, persistLayoutChange, pluginRegistry]);
445
651
 
446
652
  const openModeRoute = useCallback((
447
- screen: "ticker-search" | "themes" | "plugins" | "layout" | "new-pane",
653
+ screen: "ticker-search" | "themes" | "plugins" | "layout",
448
654
  initialQuery = "",
449
655
  payload?: Record<string, unknown>,
450
656
  ) => {
@@ -470,6 +676,14 @@ export function CommandBar({
470
676
  inputRef.current?.focus?.();
471
677
  }, []);
472
678
 
679
+ const setWorkflowNativeSelectRef = useCallback((fieldId: string, element: NativeSelectElement | null) => {
680
+ if (element) {
681
+ workflowNativeSelectRefs.current.set(fieldId, element);
682
+ } else {
683
+ workflowNativeSelectRefs.current.delete(fieldId);
684
+ }
685
+ }, []);
686
+
473
687
  const openWorkflowRoute = useCallback((route: CommandBarWorkflowRoute) => {
474
688
  pushRoute({
475
689
  ...route,
@@ -659,17 +873,10 @@ export function CommandBar({
659
873
  throw new Error(`Unknown broker "${brokerId}".`);
660
874
  }
661
875
 
662
- const requiredFields = resolveBrokerConfigFields(adapter, values).filter((field) => field.required);
663
- for (const field of requiredFields) {
664
- const nextValue = String(values[field.key] ?? "").trim();
665
- if (!nextValue) {
666
- throw new Error(`${field.label} is required.`);
667
- }
668
- }
876
+ const validationError = validateBrokerProfileValues(adapter, values);
877
+ if (validationError) throw new Error(validationError);
669
878
 
670
- const brokerValues = brokerId === "ibkr"
671
- ? buildIbkrConfigFromValues(values)
672
- : values;
879
+ const brokerValues = buildBrokerProfileConfig(adapter, values);
673
880
  const instance = await pluginRegistry.createBrokerInstanceFn(
674
881
  brokerId,
675
882
  adapter.name.trim(),
@@ -1242,7 +1449,7 @@ export function CommandBar({
1242
1449
  pluginRegistry,
1243
1450
  ]);
1244
1451
 
1245
- const runTickerSearchShortcut = useCallback(async (query?: string) => {
1452
+ const runSecurityDescriptionShortcut = useCallback(async (query?: string) => {
1246
1453
  const trimmed = query?.trim() || "";
1247
1454
  if (!trimmed) {
1248
1455
  const inferred = normalizeTickerInput(activeTickerSymbol, query);
@@ -1647,9 +1854,18 @@ export function CommandBar({
1647
1854
  if (launch.kind === "plugin-command") {
1648
1855
  const command = pluginRegistry.commands.get(launch.commandId);
1649
1856
  if (!command?.wizard || command.wizard.length === 0) return;
1650
- openPluginCommandWorkflow(command);
1857
+ let values: Record<string, string> | undefined;
1858
+ try {
1859
+ values = command.shortcutArg?.parse?.("", {
1860
+ activeTicker: activeTickerSymbol,
1861
+ });
1862
+ } catch {
1863
+ values = undefined;
1864
+ }
1865
+ openPluginCommandWorkflow(command, values ? { values } : undefined);
1651
1866
  }
1652
1867
  }, [
1868
+ activeTickerSymbol,
1653
1869
  openPluginCommandWorkflow,
1654
1870
  pluginRegistry,
1655
1871
  state.commandBarLaunchRequest,
@@ -1959,9 +2175,9 @@ export function CommandBar({
1959
2175
  if (pluginId && disabledPlugins.has(pluginId)) return false;
1960
2176
  const prefix = template.shortcut?.prefix?.toUpperCase();
1961
2177
  if (!prefix) return false;
1962
- if (upper !== prefix && !upper.startsWith(`${prefix} `)) return false;
1963
2178
  const arg = trimmed.slice(prefix.length).trim();
1964
2179
  const argKind = template.shortcut?.argKind ?? template.shortcut?.argPlaceholder;
2180
+ if (upper !== prefix && (!argKind || !upper.startsWith(`${prefix} `))) return false;
1965
2181
  if (!template.canCreate) return true;
1966
2182
  try {
1967
2183
  const canCreate = template.canCreate(context, arg ? { arg } : undefined);
@@ -1994,11 +2210,11 @@ export function CommandBar({
1994
2210
 
1995
2211
  const rootShortcutIntent = useMemo(() => parseRootShortcutIntent({
1996
2212
  query: rootQuery,
1997
- commands,
2213
+ commands: availableCommands,
1998
2214
  pluginCommands: getAvailablePluginCommands(),
1999
2215
  paneTemplates: getAvailablePaneShortcutTemplates(rootQuery),
2000
2216
  activeTicker: activeTickerSymbol,
2001
- }), [activeTickerSymbol, getAvailablePaneShortcutTemplates, getAvailablePluginCommands, rootQuery]);
2217
+ }), [activeTickerSymbol, availableCommands, getAvailablePaneShortcutTemplates, getAvailablePluginCommands, rootQuery]);
2002
2218
 
2003
2219
  const createPaneTemplateItem = useCallback((
2004
2220
  template: PaneTemplateDef,
@@ -2035,7 +2251,7 @@ export function CommandBar({
2035
2251
  id: `pane-template:${template.id}:${arg || ""}`,
2036
2252
  label: displayLabel,
2037
2253
  detail: shortcutLabel ? `${template.description} · ${shortcutLabel}` : template.description,
2038
- category: options?.category ?? (pluginName ? `${pluginName} Panes` : "New Panes"),
2254
+ category: options?.category ?? (pluginName ? `${pluginName} Panes` : "Panes"),
2039
2255
  kind: "action",
2040
2256
  right: options?.showShortcut ? template.shortcut?.prefix : undefined,
2041
2257
  searchText: `${displayLabel} ${template.label} ${template.paneId} ${template.keywords?.join(" ") || ""} ${shortcutLabel || ""} ${pluginName || ""}`,
@@ -2043,13 +2259,6 @@ export function CommandBar({
2043
2259
  };
2044
2260
  }, [openPaneTemplateDirect, openPaneTemplateWorkflow, pluginRegistry, runPaneTemplateShortcut, shouldOpenTemplateConfig]);
2045
2261
 
2046
- const paneTemplateItems = useCallback((filterQuery?: string): ResultItem[] => {
2047
- const items = getAvailablePaneTemplates().map((template) => createPaneTemplateItem(template));
2048
- return filterQuery
2049
- ? fuzzyFilter(items, filterQuery, (item) => `${item.label} ${item.detail} ${item.searchText || ""} ${item.right || ""}`)
2050
- : items;
2051
- }, [createPaneTemplateItem, getAvailablePaneTemplates]);
2052
-
2053
2262
  const paneShortcutItems = useCallback((options?: {
2054
2263
  filterQuery?: string;
2055
2264
  createOptions?: PaneTemplateCreateOptions;
@@ -2096,10 +2305,9 @@ export function CommandBar({
2096
2305
  command: CommandDef,
2097
2306
  shortcutArg: string,
2098
2307
  ): Record<string, string> => {
2099
- if (!shortcutArg.trim()) return {};
2100
2308
  return command.shortcutArg?.parse?.(shortcutArg, {
2101
2309
  activeTicker: activeTickerSymbol,
2102
- }) ?? { shortcut: shortcutArg };
2310
+ }) ?? (shortcutArg.trim() ? { shortcut: shortcutArg } : {});
2103
2311
  }, [activeTickerSymbol]);
2104
2312
 
2105
2313
  const createPluginCommandItem = useCallback((
@@ -2144,7 +2352,8 @@ export function CommandBar({
2144
2352
  return;
2145
2353
  }
2146
2354
  if (command.wizard && command.wizard.length > 0) {
2147
- openPluginCommandWorkflow(command);
2355
+ const values = parsePluginCommandShortcutArg(command, "");
2356
+ openPluginCommandWorkflow(command, { values });
2148
2357
  return;
2149
2358
  }
2150
2359
  const confirm = resolvePluginCommandConfirm(command);
@@ -2304,7 +2513,7 @@ export function CommandBar({
2304
2513
  });
2305
2514
  return;
2306
2515
  case "export-config": {
2307
- const exportPath = `${process.env.HOME || "~"}/gloomberb-config-backup.json`;
2516
+ const exportPath = getDefaultConfigBackupPath();
2308
2517
  void exportConfig(state.config, exportPath)
2309
2518
  .then(() => {
2310
2519
  notify(`Config exported to ${exportPath}`, { type: "success" });
@@ -2316,7 +2525,7 @@ export function CommandBar({
2316
2525
  return;
2317
2526
  }
2318
2527
  case "import-config": {
2319
- const importPath = `${process.env.HOME || "~"}/gloomberb-config-backup.json`;
2528
+ const importPath = getDefaultConfigBackupPath();
2320
2529
  void importConfig(state.config.dataDir, importPath)
2321
2530
  .then((imported) => {
2322
2531
  dispatch({ type: "SET_CONFIG", config: imported });
@@ -2350,8 +2559,8 @@ export function CommandBar({
2350
2559
  void onCheckForUpdates?.();
2351
2560
  closeAll({ revertThemePreview: false });
2352
2561
  return;
2353
- case "search-ticker":
2354
- void runTickerSearchShortcut(arg);
2562
+ case "security-description":
2563
+ void runSecurityDescriptionShortcut(arg);
2355
2564
  return;
2356
2565
  case "remove-watchlist":
2357
2566
  case "remove-portfolio": {
@@ -2384,12 +2593,12 @@ export function CommandBar({
2384
2593
  onCheckForUpdates,
2385
2594
  pluginRegistry,
2386
2595
  quitApp,
2387
- runTickerSearchShortcut,
2596
+ runSecurityDescriptionShortcut,
2388
2597
  executeCollectionCommand,
2389
2598
  ]);
2390
2599
 
2391
- const activeMatch = matchPrefix(rootQuery);
2392
- const rootTickerSearchArg = activeMatch?.command.id === "search-ticker" && activeMatch.arg.length >= 1
2600
+ const activeMatch = matchPrefix(rootQuery, availableCommands);
2601
+ const rootSecurityDescriptionArg = activeMatch?.command.id === "security-description" && activeMatch.arg.length >= 1
2393
2602
  ? activeMatch.arg
2394
2603
  : null;
2395
2604
 
@@ -2406,18 +2615,17 @@ export function CommandBar({
2406
2615
  if (currentRoute) return;
2407
2616
 
2408
2617
  const previousMode = previousRootModeRef.current;
2409
- if (rootModeInfo.kind === "themes" && previousMode !== "themes") {
2618
+ if (rootModeInfo.kind === "themes" && (previousMode !== "themes" || !rootThemeBaseIdRef.current)) {
2410
2619
  rootThemeBaseIdRef.current = state.config.theme;
2411
2620
  } else if (rootModeInfo.kind !== "themes" && previousMode === "themes") {
2412
2621
  const rootThemeBaseId = rootThemeBaseIdRef.current;
2413
- if (rootThemeBaseId && state.config.theme !== rootThemeBaseId) {
2414
- applyTheme(rootThemeBaseId);
2415
- dispatch({ type: "SET_THEME", theme: rootThemeBaseId });
2622
+ if (rootThemeBaseId && getEffectiveThemeId(state) !== rootThemeBaseId) {
2623
+ clearThemePreview(rootThemeBaseId);
2416
2624
  }
2417
2625
  rootThemeBaseIdRef.current = null;
2418
2626
  }
2419
2627
  previousRootModeRef.current = rootModeInfo.kind;
2420
- }, [currentRoute, dispatch, rootModeInfo.kind, state.config.theme]);
2628
+ }, [clearThemePreview, currentRoute, rootModeInfo.kind, state.config.theme, state.themePreview]);
2421
2629
 
2422
2630
  const tickerSearchRouteQuery = currentRoute?.kind === "mode" && currentRoute.screen === "ticker-search"
2423
2631
  ? currentRoute.query
@@ -2597,9 +2805,9 @@ export function CommandBar({
2597
2805
  case "check-for-updates":
2598
2806
  if (state.updateProgress?.phase === "downloading") return `Downloading v${state.updateAvailable?.version}: ${state.updateProgress.percent ?? 0}%`;
2599
2807
  if (state.updateProgress?.phase === "replacing") return "Installing update";
2600
- if (state.updateProgress?.phase === "done") return "Update installed - restart to apply";
2808
+ if (state.updateProgress?.phase === "done") return state.updateProgress.message ?? "Update installed - restart to apply";
2601
2809
  if (state.updateProgress?.phase === "error") return `Update failed: ${state.updateProgress.error}`;
2602
- if (state.updateCheckInProgress) return "Checking GitHub releases now";
2810
+ if (state.updateCheckInProgress) return "Checking releases now";
2603
2811
  if (state.updateAvailable) return `Latest available: v${state.updateAvailable.version}`;
2604
2812
  if (state.updateNotice) return state.updateNotice;
2605
2813
  return command.description;
@@ -2651,17 +2859,17 @@ export function CommandBar({
2651
2859
  }
2652
2860
 
2653
2861
  const { command } = rootShortcutIntent;
2654
- if (command.id === "search-ticker") {
2862
+ if (command.id === "security-description") {
2655
2863
  const inferredSymbol = normalizeTickerInput(activeTickerSymbol, undefined);
2656
2864
  if (!rootShortcutIntent.argText && inferredSymbol) {
2657
2865
  return {
2658
- id: "search-ticker:inferred",
2866
+ id: "security-description:inferred",
2659
2867
  label: inferredSymbol,
2660
- detail: `Open ${inferredSymbol}`,
2868
+ detail: `Open security details for ${inferredSymbol}`,
2661
2869
  category: "Search",
2662
2870
  kind: "action",
2663
2871
  right: command.prefix,
2664
- action: () => { void runTickerSearchShortcut(inferredSymbol); },
2872
+ action: () => { void runSecurityDescriptionShortcut(inferredSymbol); },
2665
2873
  };
2666
2874
  }
2667
2875
  return null;
@@ -2724,13 +2932,29 @@ export function CommandBar({
2724
2932
  }
2725
2933
 
2726
2934
  const items: ResultItem[] = [];
2727
- const match = matchPrefix(rootQuery);
2935
+ const match = matchPrefix(rootQuery, availableCommands);
2728
2936
  let initialIdx = 0;
2729
2937
  const shortcutItem = buildRootShortcutItem();
2730
2938
 
2731
- if (
2939
+ if (rootShortcutIntent.kind !== "none" && rootShortcutIntent.source === "pane-template" && shortcutItem) {
2940
+ const matchingTemplates = getAvailablePaneShortcutTemplates(rootQuery);
2941
+ const templateItems = matchingTemplates.length > 0
2942
+ ? matchingTemplates.map((template) => createPaneTemplateItem(template, {
2943
+ category: "Panes",
2944
+ createOptions: rootShortcutIntent.argText ? { arg: rootShortcutIntent.argText } : undefined,
2945
+ showShortcut: true,
2946
+ shortcutExecution: true,
2947
+ }))
2948
+ : [shortcutItem];
2949
+ const seenItemIds = new Set<string>();
2950
+ for (const item of templateItems) {
2951
+ if (seenItemIds.has(item.id)) continue;
2952
+ seenItemIds.add(item.id);
2953
+ items.push(item);
2954
+ }
2955
+ } else if (
2732
2956
  rootShortcutIntent.kind !== "none"
2733
- && (rootShortcutIntent.source === "pane-template" || rootShortcutIntent.source === "plugin-command")
2957
+ && rootShortcutIntent.source === "plugin-command"
2734
2958
  && shortcutItem
2735
2959
  ) {
2736
2960
  items.push(shortcutItem);
@@ -2768,8 +2992,6 @@ export function CommandBar({
2768
2992
  action: toggleAction,
2769
2993
  });
2770
2994
  }
2771
- } else if (match && match.command.id === "new-pane") {
2772
- items.push(...paneTemplateItems(match.arg));
2773
2995
  } else if (match && match.command.id === "layout") {
2774
2996
  const currentLayout = state.config.layout;
2775
2997
  const focusedPane = state.focusedPaneId ? findPaneInstance(currentLayout, state.focusedPaneId) : null;
@@ -3052,7 +3274,7 @@ export function CommandBar({
3052
3274
  },
3053
3275
  });
3054
3276
  });
3055
- } else if (match && match.command.id === "search-ticker") {
3277
+ } else if (match && match.command.id === "security-description") {
3056
3278
  if (shortcutItem) {
3057
3279
  items.push(shortcutItem);
3058
3280
  }
@@ -3060,7 +3282,7 @@ export function CommandBar({
3060
3282
  items.push({
3061
3283
  id: "search-hint",
3062
3284
  label: "Type a ticker symbol",
3063
- detail: "Search Yahoo Finance and connected brokers",
3285
+ detail: "Open security details after resolving a ticker",
3064
3286
  category: "Search",
3065
3287
  kind: "info",
3066
3288
  action: () => {},
@@ -3091,7 +3313,7 @@ export function CommandBar({
3091
3313
  category: "Tickers",
3092
3314
  })));
3093
3315
  items.push(...paneShortcutItems());
3094
- for (const command of commands) {
3316
+ for (const command of availableCommands) {
3095
3317
  const item = commandToItem(command);
3096
3318
  if (item) items.push(item);
3097
3319
  }
@@ -3099,7 +3321,7 @@ export function CommandBar({
3099
3321
  items.push(...pluginCommandItems());
3100
3322
  } else {
3101
3323
  const tickerItems = localTickerSearchResultItems(undefined, { category: "Tickers" });
3102
- const commandItems = commands
3324
+ const commandItems = availableCommands
3103
3325
  .map((command) => commandToItem(command))
3104
3326
  .filter((item): item is ResultItem => item !== null);
3105
3327
  const allItems = [
@@ -3118,6 +3340,7 @@ export function CommandBar({
3118
3340
  activeCollectionId,
3119
3341
  activeTickerData,
3120
3342
  activeTickerSymbol,
3343
+ availableCommands,
3121
3344
  closeAll,
3122
3345
  createPluginCommandItem,
3123
3346
  currentRoute,
@@ -3130,7 +3353,6 @@ export function CommandBar({
3130
3353
  openBuiltInWorkflow,
3131
3354
  openPickerRoute,
3132
3355
  paneShortcutItems,
3133
- paneTemplateItems,
3134
3356
  persistLayoutChange,
3135
3357
  pluginCommandItems,
3136
3358
  pluginRegistry,
@@ -3138,7 +3360,7 @@ export function CommandBar({
3138
3360
  rootQuery,
3139
3361
  rootShortcutIntent,
3140
3362
  runDirectCommand,
3141
- runTickerSearchShortcut,
3363
+ runSecurityDescriptionShortcut,
3142
3364
  state,
3143
3365
  tickerActionItems,
3144
3366
  ]);
@@ -3175,7 +3397,7 @@ export function CommandBar({
3175
3397
  return;
3176
3398
  }
3177
3399
 
3178
- if (!rootTickerSearchArg) {
3400
+ if (!rootSecurityDescriptionArg) {
3179
3401
  setRootSearching(false);
3180
3402
  setRootProviderResults(null);
3181
3403
  setRootProviderResultsQuery(null);
@@ -3184,7 +3406,7 @@ export function CommandBar({
3184
3406
  return;
3185
3407
  }
3186
3408
 
3187
- const searchQuery = rootTickerSearchArg;
3409
+ const searchQuery = rootSecurityDescriptionArg;
3188
3410
  if (rootSearchTimerRef.current) clearTimeout(rootSearchTimerRef.current);
3189
3411
  if (rootLastSearchedQueryRef.current === searchQuery) {
3190
3412
  return;
@@ -3250,18 +3472,18 @@ export function CommandBar({
3250
3472
  currentRoute,
3251
3473
  dataProvider,
3252
3474
  readTickerSearchCache,
3253
- rootTickerSearchArg,
3475
+ rootSecurityDescriptionArg,
3254
3476
  state.config.portfolios,
3255
3477
  state.tickers,
3256
3478
  writeTickerSearchCache,
3257
3479
  ]);
3258
3480
 
3259
3481
  const rootResults = useMemo(() => {
3260
- if (rootTickerSearchArg && rootProviderResultsQuery === rootTickerSearchArg && rootProviderResults) {
3482
+ if (rootSecurityDescriptionArg && rootProviderResultsQuery === rootSecurityDescriptionArg && rootProviderResults) {
3261
3483
  return rootProviderResults;
3262
3484
  }
3263
3485
  return rootResultModel.items;
3264
- }, [rootProviderResults, rootProviderResultsQuery, rootResultModel.items, rootTickerSearchArg]);
3486
+ }, [rootProviderResults, rootProviderResultsQuery, rootResultModel.items, rootSecurityDescriptionArg]);
3265
3487
 
3266
3488
  const rootGhostCompletion = !currentRoute && rootShortcutIntent.kind === "inferred-complete"
3267
3489
  ? rootShortcutIntent.completionQuery
@@ -3302,14 +3524,14 @@ export function CommandBar({
3302
3524
  : `Shortcut: ${rootShortcutIntent.label}`;
3303
3525
  }
3304
3526
 
3305
- if (rootShortcutIntent.command.id === "search-ticker") {
3527
+ if (rootShortcutIntent.command.id === "security-description") {
3306
3528
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
3307
3529
  if (symbol) {
3308
3530
  return rootShortcutIntent.kind === "inferred-complete"
3309
- ? `Shortcut: Open ${symbol} · Tab to accept`
3310
- : `Shortcut: Open ${symbol}`;
3531
+ ? `Shortcut: DES ${symbol} · Tab to accept`
3532
+ : `Shortcut: DES ${symbol}`;
3311
3533
  }
3312
- return "Shortcut: Search ticker";
3534
+ return "Shortcut: Description";
3313
3535
  }
3314
3536
 
3315
3537
  if (isCollectionCommand(rootShortcutIntent.command.id)) {
@@ -3351,7 +3573,7 @@ export function CommandBar({
3351
3573
  const acceptRootShortcutTab = useCallback((): boolean => {
3352
3574
  const intent = parseRootShortcutIntent({
3353
3575
  query: rootQueryRef.current,
3354
- commands,
3576
+ commands: availableCommands,
3355
3577
  pluginCommands: getAvailablePluginCommands(),
3356
3578
  paneTemplates: getAvailablePaneShortcutTemplates(rootQueryRef.current),
3357
3579
  activeTicker: activeTickerSymbol,
@@ -3381,7 +3603,7 @@ export function CommandBar({
3381
3603
  return false;
3382
3604
  }
3383
3605
 
3384
- if (intent.command.id === "search-ticker") {
3606
+ if (intent.command.id === "security-description") {
3385
3607
  openModeRoute("ticker-search", intent.argText);
3386
3608
  return true;
3387
3609
  }
@@ -3395,6 +3617,7 @@ export function CommandBar({
3395
3617
  return false;
3396
3618
  }, [
3397
3619
  activeTickerSymbol,
3620
+ availableCommands,
3398
3621
  getAvailablePluginCommands,
3399
3622
  getAvailablePaneShortcutTemplates,
3400
3623
  openModeRoute,
@@ -3405,7 +3628,7 @@ export function CommandBar({
3405
3628
  const resolveImmediateRootSelection = useCallback((query: string): ResultItem | null => {
3406
3629
  const intent = parseRootShortcutIntent({
3407
3630
  query,
3408
- commands,
3631
+ commands: availableCommands,
3409
3632
  pluginCommands: getAvailablePluginCommands(),
3410
3633
  paneTemplates: getAvailablePaneShortcutTemplates(query),
3411
3634
  activeTicker: activeTickerSymbol,
@@ -3425,49 +3648,45 @@ export function CommandBar({
3425
3648
  });
3426
3649
  }
3427
3650
 
3428
- const match = matchPrefix(query);
3651
+ const match = matchPrefix(query, availableCommands);
3429
3652
  if (!match) {
3430
3653
  return null;
3431
3654
  }
3432
3655
 
3433
- if (match.command.id === "search-ticker") {
3656
+ if (match.command.id === "security-description") {
3434
3657
  if (!match.arg) {
3435
3658
  const inferredTicker = normalizeTickerInput(activeTickerSymbol, undefined);
3436
3659
  if (inferredTicker) {
3437
3660
  return {
3438
- id: "search-ticker:inferred",
3661
+ id: "security-description:inferred",
3439
3662
  label: inferredTicker,
3440
- detail: `Open ${inferredTicker}`,
3663
+ detail: `Open security details for ${inferredTicker}`,
3441
3664
  category: "Search",
3442
3665
  kind: "action",
3443
3666
  right: match.command.prefix,
3444
- action: () => { void runTickerSearchShortcut(inferredTicker); },
3667
+ action: () => { void runSecurityDescriptionShortcut(inferredTicker); },
3445
3668
  };
3446
3669
  }
3447
3670
  return {
3448
- id: "search-ticker-route",
3449
- label: "Search Ticker",
3450
- detail: "Search Yahoo Finance and connected brokers",
3671
+ id: "security-description-route",
3672
+ label: "Description",
3673
+ detail: "Open security details for a ticker",
3451
3674
  category: "Search",
3452
3675
  kind: "command",
3453
3676
  action: () => openModeRoute("ticker-search", ""),
3454
3677
  };
3455
3678
  }
3456
3679
  return {
3457
- id: `search-ticker:${match.arg}`,
3458
- label: `Open ${match.arg.toUpperCase()}`,
3459
- detail: "Resolve the symbol exactly or open inline search",
3680
+ id: `security-description:${match.arg}`,
3681
+ label: `DES ${match.arg.toUpperCase()}`,
3682
+ detail: "Open security details or resolve the ticker",
3460
3683
  category: "Search",
3461
3684
  kind: "command",
3462
3685
  right: match.command.prefix,
3463
- action: () => { void runTickerSearchShortcut(match.arg); },
3686
+ action: () => { void runSecurityDescriptionShortcut(match.arg); },
3464
3687
  };
3465
3688
  }
3466
3689
 
3467
- if (match.command.id === "new-pane") {
3468
- return paneTemplateItems(match.arg)[0] ?? null;
3469
- }
3470
-
3471
3690
  if (match.command.id === "theme") {
3472
3691
  return {
3473
3692
  id: "theme-route",
@@ -3534,13 +3753,13 @@ export function CommandBar({
3534
3753
  createPaneTemplateItem,
3535
3754
  createPluginCommandItem,
3536
3755
  activeTickerSymbol,
3756
+ availableCommands,
3537
3757
  executeCollectionCommand,
3538
3758
  getAvailablePluginCommands,
3539
3759
  getAvailablePaneShortcutTemplates,
3540
3760
  openModeRoute,
3541
- paneTemplateItems,
3542
3761
  runDirectCommand,
3543
- runTickerSearchShortcut,
3762
+ runSecurityDescriptionShortcut,
3544
3763
  ]);
3545
3764
 
3546
3765
  const routeListState = useMemo<ListScreenState | null>(() => {
@@ -3548,7 +3767,7 @@ export function CommandBar({
3548
3767
  const emptyState = getEmptyState(
3549
3768
  rootModeInfo.kind,
3550
3769
  rootQuery,
3551
- activeMatch?.command.id === "search-ticker" ? activeMatch.arg : undefined,
3770
+ activeMatch?.command.id === "security-description" ? activeMatch.arg : undefined,
3552
3771
  );
3553
3772
  return {
3554
3773
  kind: "root",
@@ -3904,23 +4123,6 @@ export function CommandBar({
3904
4123
  footerRight: getScreenFooterRight(currentRoute),
3905
4124
  };
3906
4125
  }
3907
- case "new-pane": {
3908
- const results = paneTemplateItems(currentRoute.query);
3909
- return {
3910
- kind: "mode",
3911
- title: "New Pane",
3912
- subtitle: "Create panes from plugin-defined templates.",
3913
- query: currentRoute.query,
3914
- selectedIdx: currentRoute.selectedIdx,
3915
- hoveredIdx: currentRoute.hoveredIdx,
3916
- results: orderListResults(results),
3917
- searching: false,
3918
- emptyLabel: getEmptyState("new-pane", currentRoute.query).label,
3919
- emptyDetail: getEmptyState("new-pane", currentRoute.query).detail,
3920
- footerLeft: getScreenFooterLeft(currentRoute),
3921
- footerRight: getScreenFooterRight(currentRoute),
3922
- };
3923
- }
3924
4126
  case "ticker-search": {
3925
4127
  const results = currentRoute.query.trim()
3926
4128
  ? tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload))
@@ -3928,8 +4130,8 @@ export function CommandBar({
3928
4130
  const emptyState = getEmptyState("search", currentRoute.query, currentRoute.query);
3929
4131
  return {
3930
4132
  kind: "mode",
3931
- title: "Search Ticker",
3932
- subtitle: "Search Yahoo Finance and connected brokers.",
4133
+ title: "Security Description",
4134
+ subtitle: "Resolve a ticker, then open its detail pane.",
3933
4135
  query: currentRoute.query,
3934
4136
  selectedIdx: currentRoute.selectedIdx,
3935
4137
  hoveredIdx: currentRoute.hoveredIdx,
@@ -4022,6 +4224,7 @@ export function CommandBar({
4022
4224
  dispatch,
4023
4225
  duplicatePane,
4024
4226
  executeCollectionCommand,
4227
+ getAvailablePaneShortcutTemplates,
4025
4228
  getAvailablePaneTemplates,
4026
4229
  localTickerSearchResultItems,
4027
4230
  mapTickerSearchCandidateToResultItem,
@@ -4032,7 +4235,6 @@ export function CommandBar({
4032
4235
  openPaneSettingsRoute,
4033
4236
  openPickerRoute,
4034
4237
  paneShortcutItems,
4035
- paneTemplateItems,
4036
4238
  persistLayoutChange,
4037
4239
  pluginCommandItems,
4038
4240
  pluginRegistry,
@@ -4052,21 +4254,6 @@ export function CommandBar({
4052
4254
  ]);
4053
4255
  visibleListStateRef.current = routeListState;
4054
4256
 
4055
- useEffect(() => {
4056
- if (!state.commandBarOpen) return;
4057
- if (!routeListState) return;
4058
- const selected = currentRoute?.kind === "mode" && currentRoute.screen === "themes"
4059
- ? routeListState.results[currentRoute.selectedIdx]
4060
- : !currentRoute && rootModeInfo.kind === "themes"
4061
- ? routeListState.results[rootSelectedIdx]
4062
- : null;
4063
- if (!selected?.themeId) return;
4064
- if (selected?.themeId && state.config.theme !== selected.themeId) {
4065
- applyTheme(selected.themeId);
4066
- dispatch({ type: "SET_THEME", theme: selected.themeId });
4067
- }
4068
- }, [currentRoute, dispatch, rootModeInfo.kind, rootSelectedIdx, routeListState, state.commandBarOpen, state.config.theme]);
4069
-
4070
4257
  useEffect(() => {
4071
4258
  if (currentRoute?.kind !== "workflow") return;
4072
4259
  ensureRouteFieldFocus(currentRoute);
@@ -4092,6 +4279,10 @@ export function CommandBar({
4092
4279
 
4093
4280
  const setActiveListQuery = useCallback((nextQuery: string) => {
4094
4281
  if (!currentRoute) {
4282
+ if (rootModeInfo.kind === "themes" && resolveCommandBarMode(nextQuery, availableCommands).kind !== "themes") {
4283
+ clearThemePreview(rootThemeBaseIdRef.current ?? stateRef.current.config.theme);
4284
+ rootThemeBaseIdRef.current = null;
4285
+ }
4095
4286
  setRootQuery(nextQuery);
4096
4287
  return;
4097
4288
  }
@@ -4104,14 +4295,18 @@ export function CommandBar({
4104
4295
  return route;
4105
4296
  });
4106
4297
  }
4107
- }, [currentRoute, setRootQuery, updateTopRoute]);
4298
+ }, [availableCommands, clearThemePreview, currentRoute, rootModeInfo.kind, setRootQuery, updateTopRoute]);
4108
4299
 
4109
4300
  const moveListSelection = useCallback((delta: number) => {
4110
4301
  const listState = visibleListStateRef.current;
4111
4302
  if (!listState || listState.results.length === 0 || delta === 0) return;
4112
- const maxIndex = listState.results.length - 1;
4303
+ const nextIndex = clampListIndex(listState.selectedIdx + delta, listState.results.length);
4304
+ const nextListState = { ...listState, selectedIdx: nextIndex, hoveredIdx: null };
4305
+ visibleListStateRef.current = nextListState;
4306
+ previewThemeSelection(nextListState, nextIndex);
4307
+
4113
4308
  if (!currentRouteRef.current) {
4114
- setRootSelectedIdx((current) => Math.max(0, Math.min(current + delta, maxIndex)));
4309
+ setRootSelectedIdx(nextIndex);
4115
4310
  setRootHoveredIdx(null);
4116
4311
  return;
4117
4312
  }
@@ -4122,19 +4317,19 @@ export function CommandBar({
4122
4317
  if (!top || (top.kind !== "mode" && top.kind !== "picker" && top.kind !== "pane-settings")) {
4123
4318
  return current;
4124
4319
  }
4125
- const nextIndex = Math.max(0, Math.min(top.selectedIdx + delta, maxIndex));
4126
4320
  next[next.length - 1] = { ...top, selectedIdx: nextIndex, hoveredIdx: null };
4127
4321
  return next;
4128
4322
  });
4129
- }, []);
4323
+ }, [previewThemeSelection]);
4130
4324
 
4131
4325
  const setHoveredIndex = useCallback((index: number | null) => {
4132
4326
  if (!currentRoute) {
4133
- setRootHoveredIdx(index);
4327
+ setRootHoveredIdx((current) => (current === index ? current : index));
4134
4328
  return;
4135
4329
  }
4136
4330
  updateTopRoute((route) => {
4137
4331
  if (route.kind === "mode" || route.kind === "picker" || route.kind === "pane-settings") {
4332
+ if (route.hoveredIdx === index) return route;
4138
4333
  return { ...route, hoveredIdx: index };
4139
4334
  }
4140
4335
  return route;
@@ -4185,6 +4380,111 @@ export function CommandBar({
4185
4380
  });
4186
4381
  }, []);
4187
4382
 
4383
+ const alertWorkflowActive = isSetAlertWorkflow(currentRoute);
4384
+ const alertWorkflowSymbol = alertWorkflowActive
4385
+ ? normalizeAlertWorkflowSymbol(currentRoute.values.symbol)
4386
+ : "";
4387
+
4388
+ useEffect(() => {
4389
+ if (!alertWorkflowActive) return;
4390
+
4391
+ const requestId = alertWorkflowQuoteRequestRef.current + 1;
4392
+ alertWorkflowQuoteRequestRef.current = requestId;
4393
+
4394
+ if (!alertWorkflowSymbol) {
4395
+ updateTopRoute((route) => {
4396
+ if (!isSetAlertWorkflow(route)) return route;
4397
+ return {
4398
+ ...route,
4399
+ fields: updateAlertWorkflowFieldDescriptions(route.fields, {
4400
+ symbol: "Enter a symbol to validate it.",
4401
+ price: "Target fills from the current price after the symbol resolves.",
4402
+ }),
4403
+ payloadMeta: {
4404
+ ...(route.payloadMeta ?? {}),
4405
+ alertQuoteSymbol: "",
4406
+ alertQuoteStatus: "idle",
4407
+ },
4408
+ };
4409
+ });
4410
+ return;
4411
+ }
4412
+
4413
+ updateTopRoute((route) => {
4414
+ if (!isSetAlertWorkflow(route)) return route;
4415
+ if (normalizeAlertWorkflowSymbol(route.values.symbol) !== alertWorkflowSymbol) return route;
4416
+ return {
4417
+ ...route,
4418
+ fields: updateAlertWorkflowFieldDescriptions(route.fields, {
4419
+ symbol: `Checking ${alertWorkflowSymbol}...`,
4420
+ price: "Target fills from the current price after the symbol resolves.",
4421
+ }),
4422
+ payloadMeta: {
4423
+ ...(route.payloadMeta ?? {}),
4424
+ alertQuoteSymbol: alertWorkflowSymbol,
4425
+ alertQuoteStatus: "checking",
4426
+ },
4427
+ };
4428
+ });
4429
+
4430
+ void dataProvider.getQuote(alertWorkflowSymbol, "")
4431
+ .then((quote) => {
4432
+ if (alertWorkflowQuoteRequestRef.current !== requestId) return;
4433
+ if (!quote || typeof quote.price !== "number" || !Number.isFinite(quote.price)) {
4434
+ throw new Error(`No quote found for "${alertWorkflowSymbol}".`);
4435
+ }
4436
+
4437
+ const quotePrice = formatAlertWorkflowPrice(quote.price);
4438
+ updateTopRoute((route) => {
4439
+ if (!isSetAlertWorkflow(route)) return route;
4440
+ if (normalizeAlertWorkflowSymbol(route.values.symbol) !== alertWorkflowSymbol) return route;
4441
+
4442
+ const currentPrice = coerceFieldString(route.values.price).trim();
4443
+ const previousAutoPrice = String(route.payloadMeta?.alertAutoPrice ?? "");
4444
+ const shouldPrefill = currentPrice.length === 0 || currentPrice === previousAutoPrice;
4445
+ const nextValues = shouldPrefill
4446
+ ? { ...route.values, price: quotePrice }
4447
+ : route.values;
4448
+
4449
+ return {
4450
+ ...route,
4451
+ values: nextValues,
4452
+ fields: updateAlertWorkflowFieldDescriptions(route.fields, {
4453
+ symbol: formatAlertWorkflowQuote(quote),
4454
+ price: `Current price ${quotePrice}; edit to set the target.`,
4455
+ }),
4456
+ payloadMeta: {
4457
+ ...(route.payloadMeta ?? {}),
4458
+ alertQuoteSymbol: alertWorkflowSymbol,
4459
+ alertQuoteStatus: "valid",
4460
+ alertAutoPrice: quotePrice,
4461
+ },
4462
+ };
4463
+ });
4464
+ })
4465
+ .catch((error) => {
4466
+ if (alertWorkflowQuoteRequestRef.current !== requestId) return;
4467
+ const message = summarizeAlertWorkflowQuoteError(alertWorkflowSymbol, error);
4468
+ updateTopRoute((route) => {
4469
+ if (!isSetAlertWorkflow(route)) return route;
4470
+ if (normalizeAlertWorkflowSymbol(route.values.symbol) !== alertWorkflowSymbol) return route;
4471
+ return {
4472
+ ...route,
4473
+ fields: updateAlertWorkflowFieldDescriptions(route.fields, {
4474
+ symbol: message,
4475
+ price: undefined,
4476
+ }),
4477
+ payloadMeta: {
4478
+ ...(route.payloadMeta ?? {}),
4479
+ alertQuoteSymbol: alertWorkflowSymbol,
4480
+ alertQuoteStatus: "invalid",
4481
+ alertQuoteError: message,
4482
+ },
4483
+ };
4484
+ });
4485
+ });
4486
+ }, [alertWorkflowActive, alertWorkflowSymbol, dataProvider, updateTopRoute]);
4487
+
4188
4488
  const moveWorkflowFocus = useCallback((delta: number) => {
4189
4489
  if (currentRoute?.kind !== "workflow") return;
4190
4490
  syncActiveWorkflowTextarea(currentRoute);
@@ -4602,6 +4902,18 @@ export function CommandBar({
4602
4902
  const activeField = visibleFields.find((field) => field.id === currentRoute.activeFieldId) ?? visibleFields[0];
4603
4903
  const activeTextarea = activeField?.type === "textarea";
4604
4904
 
4905
+ if (isPlainBackspace(event)) {
4906
+ const activeValue = activeField
4907
+ ? getWorkflowFieldStringValue(activeField, currentRoute.values[activeField.id])
4908
+ : "";
4909
+ if (!activeField || !isWorkflowTextField(activeField) || activeValue.length === 0) {
4910
+ event.stopPropagation();
4911
+ event.preventDefault();
4912
+ popRoute();
4913
+ return;
4914
+ }
4915
+ }
4916
+
4605
4917
  if (event.name === "tab") {
4606
4918
  event.stopPropagation();
4607
4919
  event.preventDefault();
@@ -4641,11 +4953,19 @@ export function CommandBar({
4641
4953
  return;
4642
4954
  }
4643
4955
 
4644
- if (event.name === "return" || event.name === "enter") {
4956
+ if (
4957
+ event.name === "return"
4958
+ || event.name === "enter"
4959
+ || (nativePaneChrome && event.name === "space" && activeField?.type === "select")
4960
+ ) {
4645
4961
  if (!activeField) return;
4646
4962
  if (activeField.type === "select" || activeField.type === "multi-select" || activeField.type === "ordered-multi-select" || activeField.type === "toggle") {
4647
4963
  event.stopPropagation();
4648
4964
  event.preventDefault();
4965
+ if (nativePaneChrome && activeField.type === "select") {
4966
+ openNativeSelect(workflowNativeSelectRefs.current.get(activeField.id));
4967
+ return;
4968
+ }
4649
4969
  openWorkflowFieldPicker(currentRoute, activeField);
4650
4970
  return;
4651
4971
  }
@@ -4670,7 +4990,8 @@ export function CommandBar({
4670
4990
  if (currentRoute.pickerId === "field-multi-select" && (event.name === "space" || event.sequence === " ")) {
4671
4991
  event.stopPropagation();
4672
4992
  event.preventDefault();
4673
- const selected = routeListState?.results[currentRoute.selectedIdx];
4993
+ const listState = visibleListStateRef.current;
4994
+ const selected = listState?.results[listState.selectedIdx];
4674
4995
  if (!selected) return;
4675
4996
  handleMultiSelectToggle(selected.id);
4676
4997
  return;
@@ -4720,7 +5041,8 @@ export function CommandBar({
4720
5041
  return;
4721
5042
  }
4722
5043
 
4723
- if (!routeListState) return;
5044
+ const activeListState = visibleListStateRef.current;
5045
+ if (!activeListState) return;
4724
5046
 
4725
5047
  if (!currentRoute && event.name === "tab") {
4726
5048
  if (acceptRootShortcutTab()) {
@@ -4754,7 +5076,7 @@ export function CommandBar({
4754
5076
  if ((event.ctrl && event.name === "w") || (event.meta && (event.name === "h" || event.name === "u"))) {
4755
5077
  event.stopPropagation();
4756
5078
  event.preventDefault();
4757
- const trimmed = routeListState.query.replace(/\s+$/, "");
5079
+ const trimmed = activeListState.query.replace(/\s+$/, "");
4758
5080
  const nextQuery = trimmed.replace(/[^\s]+$/, "").replace(/\s+$/, "");
4759
5081
  setActiveListQuery(nextQuery);
4760
5082
  return;
@@ -4765,7 +5087,7 @@ export function CommandBar({
4765
5087
  if (pluginToggleMode && event.name === "space") {
4766
5088
  event.stopPropagation();
4767
5089
  event.preventDefault();
4768
- const selected = routeListState.results[routeListState.selectedIdx];
5090
+ const selected = activeListState.results[activeListState.selectedIdx];
4769
5091
  if (selected?.pluginToggle) {
4770
5092
  void selected.pluginToggle();
4771
5093
  }
@@ -4806,21 +5128,20 @@ export function CommandBar({
4806
5128
  handleMultiSelectToggle,
4807
5129
  moveListSelection,
4808
5130
  moveWorkflowFocus,
5131
+ nativePaneChrome,
4809
5132
  openWorkflowFieldPicker,
4810
5133
  popRoute,
4811
5134
  renderer,
4812
5135
  rootModeInfo.kind,
4813
- routeListState,
4814
5136
  setActiveListQuery,
4815
5137
  updateWorkflowValue,
4816
5138
  ]);
4817
5139
 
4818
- const barWidth = Math.max(42, Math.min(72, termWidth - 8, Math.floor(termWidth * 0.68)));
4819
- const bodyHeight = Math.min(16, Math.max(9, termHeight - 9));
4820
- const barHeight = bodyHeight + 7;
4821
- const barLeft = Math.max(4, Math.floor((termWidth - barWidth) / 2));
4822
- const barTop = Math.max(1, Math.floor((termHeight - barHeight) / 2));
4823
- const contentPadding = 3;
5140
+ const barWidth = nativePaneChrome
5141
+ ? Math.max(46, Math.min(78, termWidth - 10, Math.floor(termWidth * 0.64)))
5142
+ : Math.max(42, Math.min(72, termWidth - 8, Math.floor(termWidth * 0.68)));
5143
+ const baseBodyHeight = Math.min(16, Math.max(9, termHeight - 9));
5144
+ const contentPadding = nativePaneChrome ? 1 : 3;
4824
5145
  const paletteBg = commandBarBg();
4825
5146
  const paletteHeadingText = commandBarHeadingText();
4826
5147
  const paletteHoverBg = commandBarHoverBg();
@@ -4828,98 +5149,138 @@ export function CommandBar({
4828
5149
  const paletteSelectedText = commandBarSelectedText();
4829
5150
  const paletteText = commandBarText();
4830
5151
  const paletteSubtleText = commandBarSubtleText();
4831
- const resultsInnerWidth = Math.max(12, barWidth - contentPadding * 2);
4832
- const trailingWidth = Math.max(8, Math.min(12, Math.floor(resultsInnerWidth * 0.18)));
4833
- const labelWidth = Math.max(10, resultsInnerWidth - trailingWidth);
4834
- const queryDisplayWidth = Math.max(8, barWidth - contentPadding * 2);
5152
+ const panelBg = nativePaneChrome ? commandBarPanelBg() : paletteBg;
5153
+ const inputBg = nativePaneChrome ? commandBarInputBg() : paletteBg;
4835
5154
  const visibleListState = routeListState && (routeListState.kind === "root" || routeListState.kind === "mode" || routeListState.kind === "picker" || routeListState.kind === "pane-settings")
4836
5155
  ? routeListState
4837
5156
  : null;
4838
5157
  const showCustomMultiSelectPicker = currentRoute?.kind === "picker" && currentRoute.pickerId === "field-multi-select";
5158
+ const listRows = visibleListState ? buildListRows(visibleListState) : [];
5159
+ const nativeListRows = visibleListState ? buildNativeListRows(visibleListState, listRows) : [];
5160
+ const workflowBodyHeight = currentRoute?.kind === "workflow"
5161
+ ? Math.min(
5162
+ Math.max(9, termHeight - (nativePaneChrome ? 7 : 9)),
5163
+ Math.max(7, estimateWorkflowBodyRows(currentRoute)),
5164
+ )
5165
+ : baseBodyHeight;
5166
+ const bodyHeight = currentRoute?.kind === "workflow" ? workflowBodyHeight : baseBodyHeight;
5167
+ const listBodyHeight = baseBodyHeight;
5168
+ const nativePanelPaddingColumns = nativePaneChrome
5169
+ ? Math.ceil((NATIVE_COMMAND_BAR_PADDING_X_PX * 2) / Math.max(1, cellWidthPx))
5170
+ : 0;
5171
+ const nativePanelPaddingRows = nativePaneChrome
5172
+ ? Math.ceil((NATIVE_COMMAND_BAR_PADDING_Y_PX * 2) / Math.max(1, cellHeightPx))
5173
+ : 0;
5174
+ const nativeBodyChromeRows = (currentRoute?.kind === "workflow"
5175
+ || currentRoute?.kind === "confirm"
5176
+ || showCustomMultiSelectPicker)
5177
+ ? 1
5178
+ : 3;
5179
+ const barHeight = nativePaneChrome
5180
+ ? bodyHeight + nativeBodyChromeRows + nativePanelPaddingRows
5181
+ : bodyHeight + 7;
5182
+ const barLeft = Math.max(4, Math.floor((termWidth - barWidth) / 2));
5183
+ const barTop = Math.max(1, Math.floor((termHeight - barHeight) / 2));
5184
+ const resultsInnerWidth = Math.max(12, barWidth - nativePanelPaddingColumns - contentPadding * 2);
5185
+ const trailingWidth = Math.max(8, Math.min(12, Math.floor(resultsInnerWidth * 0.18)));
5186
+ const labelWidth = Math.max(10, resultsInnerWidth - trailingWidth);
5187
+ const queryDisplayWidth = Math.max(8, resultsInnerWidth);
5188
+ const nativeSelectedRowIndex = nativePaneChrome && visibleListState
5189
+ ? nativeListRows.findIndex((row) => row.kind === "item" && row.globalIdx === visibleListState.selectedIdx)
5190
+ : -1;
4839
5191
  const bodySlotKey = showCustomMultiSelectPicker
4840
5192
  ? "picker:field-multi-select"
4841
5193
  : currentRoute?.kind === "picker"
4842
5194
  ? `picker:${currentRoute.pickerId}`
4843
5195
  : currentRoute?.kind ?? "root";
4844
5196
 
5197
+ useEffect(() => {
5198
+ if (!nativePaneChrome || nativeSelectedRowIndex < 0) return;
5199
+ const scrollBox = nativeListScrollRef.current;
5200
+ if (!scrollBox) return;
5201
+ const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? listBodyHeight);
5202
+ if (nativeSelectedRowIndex < scrollBox.scrollTop) {
5203
+ scrollBox.scrollTo(nativeSelectedRowIndex);
5204
+ } else if (nativeSelectedRowIndex >= scrollBox.scrollTop + viewportHeight) {
5205
+ scrollBox.scrollTo(nativeSelectedRowIndex - viewportHeight + 1);
5206
+ }
5207
+ }, [listBodyHeight, nativePaneChrome, nativeSelectedRowIndex, visibleListState?.kind, visibleListState?.query]);
5208
+
5209
+ useEffect(() => {
5210
+ if (!nativePaneChrome || currentRoute?.kind !== "workflow") return;
5211
+ const scrollBox = workflowScrollRef.current;
5212
+ if (!scrollBox) return;
5213
+ const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
5214
+ const activeIndex = visibleFields.findIndex((field) => field.id === currentRoute.activeFieldId);
5215
+ if (activeIndex < 0) return;
5216
+
5217
+ const estimatedFieldHeight = 4;
5218
+ const fieldTop = activeIndex * estimatedFieldHeight;
5219
+ const fieldBottom = fieldTop + estimatedFieldHeight;
5220
+ const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? bodyHeight);
5221
+ if (fieldTop < scrollBox.scrollTop) {
5222
+ scrollBox.scrollTo(fieldTop);
5223
+ } else if (fieldBottom > scrollBox.scrollTop + viewportHeight) {
5224
+ scrollBox.scrollTo(fieldBottom - viewportHeight);
5225
+ }
5226
+ }, [bodyHeight, currentRoute, nativePaneChrome]);
5227
+
4845
5228
  const renderListBody = () => {
4846
5229
  if (!visibleListState) return null;
4847
5230
 
4848
- const allRows: Array<
4849
- | { kind: "spacer"; id: string }
4850
- | { kind: "heading"; id: string; label: string }
4851
- | { kind: "item"; item: ResultItem; globalIdx: number }
4852
- | { kind: "message"; id: string; label: string; dim?: boolean }
4853
- | { kind: "spinner"; id: string; label: string }
4854
- | { kind: "filler"; id: string }
4855
- > = [];
4856
- const sections = buildSections(visibleListState.results);
4857
- let globalIdx = 0;
4858
- sections.forEach((section, sectionIndex) => {
4859
- if (sectionIndex > 0) {
4860
- allRows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
4861
- }
4862
- allRows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
4863
- for (const item of section.items) {
4864
- allRows.push({ kind: "item", item, globalIdx });
4865
- globalIdx += 1;
4866
- }
4867
- });
4868
-
4869
- let visibleRows: typeof allRows;
4870
- if (visibleListState.searching && allRows.length === 0) {
5231
+ const allRows = listRows;
5232
+ let visibleRows: CommandBarListRow[];
5233
+ if (nativePaneChrome) {
5234
+ visibleRows = nativeListRows;
5235
+ } else if (visibleListState.searching && allRows.length === 0) {
4871
5236
  visibleRows = [{ kind: "spinner", id: "searching", label: "Searching…" }];
4872
5237
  } else if (allRows.length === 0) {
4873
5238
  visibleRows = [{ kind: "message", id: "empty", label: visibleListState.emptyLabel }];
4874
5239
  } else {
4875
5240
  const selectedRowIdx = allRows.findIndex((row) => row.kind === "item" && row.globalIdx === visibleListState.selectedIdx);
4876
- const halfWindow = Math.floor(bodyHeight / 2);
4877
- let windowStart = Math.max(0, Math.min(selectedRowIdx - halfWindow, allRows.length - bodyHeight));
5241
+ const halfWindow = Math.floor(listBodyHeight / 2);
5242
+ let windowStart = Math.max(0, Math.min(selectedRowIdx - halfWindow, allRows.length - listBodyHeight));
4878
5243
  if (windowStart < 0) windowStart = 0;
4879
- visibleRows = allRows.slice(windowStart, windowStart + bodyHeight);
5244
+ visibleRows = allRows.slice(windowStart, windowStart + listBodyHeight);
4880
5245
  if (visibleListState.searching) {
4881
- if (visibleRows.length >= bodyHeight) {
4882
- visibleRows = visibleRows.slice(0, bodyHeight - 1);
5246
+ if (visibleRows.length >= listBodyHeight) {
5247
+ visibleRows = visibleRows.slice(0, listBodyHeight - 1);
4883
5248
  }
4884
5249
  visibleRows.push({ kind: "spinner", id: "searching", label: "Searching…" });
4885
5250
  }
4886
5251
  }
4887
5252
 
4888
- while (visibleRows.length < bodyHeight) {
5253
+ while (!nativePaneChrome && visibleRows.length < listBodyHeight) {
4889
5254
  visibleRows.push({ kind: "filler", id: `filler:${visibleRows.length}` });
4890
5255
  }
4891
5256
 
4892
- return (
4893
- <box
4894
- flexDirection="column"
4895
- height={bodyHeight}
4896
- onMouseScroll={handleListScroll}
4897
- >
5257
+ const renderedRows = (
5258
+ <>
4898
5259
  {visibleRows.map((row) => {
4899
5260
  if (row.kind === "filler" || row.kind === "spacer") {
4900
- return <box key={row.id} height={1} />;
5261
+ return <Box key={row.id} height={1} />;
4901
5262
  }
4902
5263
  if (row.kind === "spinner") {
4903
5264
  return (
4904
- <box key={row.id} height={1} paddingX={contentPadding} onMouseScroll={handleListScroll}>
5265
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
4905
5266
  <Spinner label={row.label} />
4906
- </box>
5267
+ </Box>
4907
5268
  );
4908
5269
  }
4909
5270
  if (row.kind === "message") {
4910
5271
  return (
4911
- <box key={row.id} height={1} paddingX={contentPadding} onMouseScroll={handleListScroll}>
4912
- <text fg={paletteText}>{truncateText(row.label, barWidth - contentPadding * 2)}</text>
4913
- </box>
5272
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
5273
+ <Text fg={paletteText}>{truncateText(row.label, queryDisplayWidth)}</Text>
5274
+ </Box>
4914
5275
  );
4915
5276
  }
4916
5277
  if (row.kind === "heading") {
4917
5278
  return (
4918
- <box key={row.id} height={1} paddingX={contentPadding} onMouseScroll={handleListScroll}>
4919
- <text attributes={TextAttributes.BOLD} fg={paletteHeadingText}>
4920
- {truncateText(row.label, barWidth - contentPadding * 2)}
4921
- </text>
4922
- </box>
5279
+ <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}>
5280
+ <Text attributes={TextAttributes.BOLD} fg={paletteHeadingText}>
5281
+ {truncateText(row.label, queryDisplayWidth)}
5282
+ </Text>
5283
+ </Box>
4923
5284
  );
4924
5285
  }
4925
5286
 
@@ -4929,14 +5290,19 @@ export function CommandBar({
4929
5290
  const label = truncateText(presentation.label, labelWidth);
4930
5291
  const trailing = truncateText(presentation.trailing, trailingWidth);
4931
5292
  return (
4932
- <box
5293
+ <Box
4933
5294
  key={row.item.id}
4934
5295
  flexDirection="row"
4935
5296
  height={1}
4936
5297
  paddingX={contentPadding}
4937
- backgroundColor={isSelected ? paletteSelectedBg : isHovered ? paletteHoverBg : paletteBg}
5298
+ backgroundColor={isSelected
5299
+ ? paletteSelectedBg
5300
+ : isHovered
5301
+ ? paletteHoverBg
5302
+ : (nativePaneChrome ? panelBg : paletteBg)}
4938
5303
  onMouseMove={() => setHoveredIndex(row.globalIdx)}
4939
- onMouseScroll={handleListScroll}
5304
+ onMouseOut={() => setHoveredIndex(null)}
5305
+ {...(!nativePaneChrome ? { onMouseScroll: handleListScroll } : {})}
4940
5306
  onMouseDown={(event: any) => {
4941
5307
  event.stopPropagation?.();
4942
5308
  event.preventDefault?.();
@@ -4952,64 +5318,97 @@ export function CommandBar({
4952
5318
  }
4953
5319
  activateListSelection({ item: row.item });
4954
5320
  }}
5321
+ data-command-bar-row-selected={nativePaneChrome && isSelected ? "true" : undefined}
5322
+ style={nativePaneChrome ? { borderRadius: 6 } : undefined}
4955
5323
  >
4956
- <box width={labelWidth}>
4957
- <text fg={isSelected ? paletteSelectedText : presentation.primaryMuted ? paletteSubtleText : paletteText}>
5324
+ <Box width={labelWidth}>
5325
+ <Text fg={isSelected ? paletteSelectedText : presentation.primaryMuted ? paletteSubtleText : paletteText}>
4958
5326
  {label}
4959
- </text>
4960
- </box>
4961
- <box width={trailingWidth}>
4962
- <text fg={isSelected ? paletteSelectedText : paletteSubtleText}>{trailing}</text>
4963
- </box>
4964
- </box>
5327
+ </Text>
5328
+ </Box>
5329
+ <Box width={trailingWidth}>
5330
+ <Text fg={isSelected ? paletteSelectedText : paletteSubtleText}>{trailing}</Text>
5331
+ </Box>
5332
+ </Box>
4965
5333
  );
4966
5334
  })}
4967
- </box>
5335
+ </>
5336
+ );
5337
+
5338
+ if (nativePaneChrome) {
5339
+ return (
5340
+ <ScrollBox
5341
+ ref={nativeListScrollRef}
5342
+ flexDirection="column"
5343
+ height={listBodyHeight}
5344
+ scrollY
5345
+ >
5346
+ {renderedRows}
5347
+ </ScrollBox>
5348
+ );
5349
+ }
5350
+
5351
+ return (
5352
+ <Box
5353
+ flexDirection="column"
5354
+ height={listBodyHeight}
5355
+ onMouseScroll={handleListScroll}
5356
+ >
5357
+ {renderedRows}
5358
+ </Box>
4968
5359
  );
4969
5360
  };
4970
5361
 
4971
5362
  const renderWorkflowBody = () => {
4972
5363
  if (currentRoute?.kind !== "workflow") return null;
4973
5364
  const visibleFields = getVisibleWorkflowFields(currentRoute.fields, currentRoute.values);
4974
- return (
4975
- <box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5365
+ const workflowContent = (
5366
+ <>
4976
5367
  {currentRoute.subtitle && (
4977
- <box height={1}>
4978
- <text fg={paletteSubtleText}>{truncateText(currentRoute.subtitle, barWidth - contentPadding * 2)}</text>
4979
- </box>
5368
+ <Box height={1}>
5369
+ <Text fg={paletteSubtleText}>{truncateText(currentRoute.subtitle, queryDisplayWidth)}</Text>
5370
+ </Box>
4980
5371
  )}
4981
5372
  {currentRoute.description?.map((line, index) => (
4982
- <box key={`workflow-desc:${index}`} height={1}>
4983
- <text fg={paletteSubtleText}>{truncateText(line, barWidth - contentPadding * 2)}</text>
4984
- </box>
5373
+ <Box key={`workflow-desc:${index}`} height={1}>
5374
+ <Text fg={paletteSubtleText}>{truncateText(line, queryDisplayWidth)}</Text>
5375
+ </Box>
4985
5376
  ))}
4986
- {currentRoute.subtitle || (currentRoute.description?.length ?? 0) > 0 ? <box height={1} /> : null}
4987
- {visibleFields.map((field) => {
5377
+ {currentRoute.subtitle || (currentRoute.description?.length ?? 0) > 0 ? <Box height={1} /> : null}
5378
+ {visibleFields.map((field, fieldIndex) => {
4988
5379
  const active = field.id === currentRoute.activeFieldId;
5380
+ const isLastField = fieldIndex === visibleFields.length - 1;
4989
5381
  const value = currentRoute.values[field.id];
4990
5382
  const borderColor = active ? paletteSelectedBg : paletteBg;
5383
+ const fieldBg = nativePaneChrome ? "transparent" : active ? inputBg : panelBg;
5384
+ const useNativeSelect = nativePaneChrome && field.type === "select";
5385
+ const fieldDescription = getWorkflowFieldDescription(field, active);
4991
5386
  return (
4992
- <box
5387
+ <Box
4993
5388
  key={field.id}
4994
5389
  flexDirection="column"
4995
- marginBottom={1}
4996
- backgroundColor={active ? colors.panel : paletteBg}
5390
+ {...(!nativePaneChrome ? { marginBottom: isLastField ? 0 : 1 } : {})}
5391
+ backgroundColor={fieldBg}
4997
5392
  onMouseDown={(event: any) => {
4998
5393
  event.stopPropagation?.();
4999
5394
  syncActiveWorkflowTextarea(currentRoute);
5000
5395
  updateTopRoute((route) => route.kind === "workflow"
5001
5396
  ? { ...route, activeFieldId: field.id, error: null }
5002
5397
  : route);
5003
- if (!isWorkflowTextField(field)) {
5398
+ if (!isWorkflowTextField(field) && !useNativeSelect) {
5004
5399
  openWorkflowFieldPicker(currentRoute, field);
5005
5400
  }
5006
5401
  }}
5402
+ style={nativePaneChrome ? {
5403
+ marginBottom: isLastField ? 8 : 10,
5404
+ paddingBlock: 3,
5405
+ } : undefined}
5007
5406
  >
5008
- <box height={1}>
5009
- <text fg={active ? paletteText : paletteSubtleText} attributes={active ? TextAttributes.BOLD : 0}>
5407
+ <Box height={1}>
5408
+ <Text fg={active ? paletteText : paletteSubtleText} attributes={active ? TextAttributes.BOLD : 0}>
5010
5409
  {field.label}
5011
- </text>
5012
- </box>
5410
+ </Text>
5411
+ </Box>
5013
5412
  {isWorkflowTextField(field) ? (
5014
5413
  field.type === "number" ? (
5015
5414
  <NumberField
@@ -5017,6 +5416,8 @@ export function CommandBar({
5017
5416
  value={coerceFieldString(value)}
5018
5417
  placeholder={field.placeholder}
5019
5418
  focused={active && !currentRoute.pending}
5419
+ variant="default"
5420
+ backgroundColor={nativePaneChrome ? inputBg : fieldBg}
5020
5421
  onChange={(nextValue) => updateWorkflowValue(field.id, nextValue)}
5021
5422
  onSubmit={() => {
5022
5423
  const index = visibleFields.findIndex((entry) => entry.id === field.id);
@@ -5028,15 +5429,20 @@ export function CommandBar({
5028
5429
  }}
5029
5430
  />
5030
5431
  ) : field.type === "textarea" ? (
5031
- <box
5432
+ <Box
5032
5433
  minHeight={6}
5033
5434
  height={6}
5034
- border
5435
+ border={!nativePaneChrome}
5035
5436
  borderColor={active ? paletteSelectedBg : paletteBg}
5036
- backgroundColor={active ? colors.panel : paletteBg}
5437
+ backgroundColor={nativePaneChrome ? inputBg : fieldBg}
5438
+ style={nativePaneChrome ? {
5439
+ border: `1px solid ${active ? colors.borderFocused : colors.border}`,
5440
+ borderRadius: 6,
5441
+ overflow: "hidden",
5442
+ } : undefined}
5037
5443
  >
5038
5444
  {active ? (
5039
- <textarea
5445
+ <Textarea
5040
5446
  key={field.id}
5041
5447
  ref={getInputRef(workflowInputRefs.current, field.id) as RefObject<TextareaRenderable | null>}
5042
5448
  initialValue={coerceFieldString(value)}
@@ -5044,27 +5450,27 @@ export function CommandBar({
5044
5450
  focused={!currentRoute.pending}
5045
5451
  textColor={paletteText}
5046
5452
  placeholderColor={paletteSubtleText}
5047
- backgroundColor={colors.panel}
5453
+ backgroundColor={nativePaneChrome ? inputBg : colors.panel}
5048
5454
  flexGrow={1}
5049
5455
  wrapText
5050
5456
  />
5051
5457
  ) : (
5052
- <box flexDirection="column" paddingX={1} paddingY={0}>
5458
+ <Box flexDirection="column" paddingX={1} paddingY={0}>
5053
5459
  {(() => {
5054
5460
  const preview = coerceFieldString(value).trim();
5055
5461
  const lines = (preview || field.placeholder || "Unset")
5056
5462
  .split("\n")
5057
- .flatMap((line) => line.match(new RegExp(`.{1,${Math.max(1, barWidth - contentPadding * 2 - 8)}}`, "g")) ?? [""])
5463
+ .flatMap((line) => line.match(new RegExp(`.{1,${Math.max(1, queryDisplayWidth - 8)}}`, "g")) ?? [""])
5058
5464
  .slice(0, 4);
5059
5465
  return lines.map((line, index) => (
5060
- <box key={`${field.id}:preview:${index}`} height={1}>
5061
- <text fg={preview ? paletteText : paletteSubtleText}>{line || " "}</text>
5062
- </box>
5466
+ <Box key={`${field.id}:preview:${index}`} height={1}>
5467
+ <Text fg={preview ? paletteText : paletteSubtleText}>{line || " "}</Text>
5468
+ </Box>
5063
5469
  ));
5064
5470
  })()}
5065
- </box>
5471
+ </Box>
5066
5472
  )}
5067
- </box>
5473
+ </Box>
5068
5474
  ) : (
5069
5475
  <TextField
5070
5476
  inputRef={getInputRef(workflowInputRefs.current, field.id) as RefObject<InputRenderable | null>}
@@ -5072,6 +5478,8 @@ export function CommandBar({
5072
5478
  value={coerceFieldString(value)}
5073
5479
  placeholder={field.placeholder}
5074
5480
  focused={active && !currentRoute.pending}
5481
+ variant="default"
5482
+ backgroundColor={nativePaneChrome ? inputBg : fieldBg}
5075
5483
  onChange={(nextValue) => updateWorkflowValue(field.id, nextValue)}
5076
5484
  onSubmit={() => {
5077
5485
  const index = visibleFields.findIndex((entry) => entry.id === field.id);
@@ -5083,83 +5491,116 @@ export function CommandBar({
5083
5491
  }}
5084
5492
  />
5085
5493
  )
5494
+ ) : useNativeSelect ? (
5495
+ <NativeSelect
5496
+ value={coerceFieldString(value)}
5497
+ options={field.options}
5498
+ width="100%"
5499
+ selectRef={(element) => setWorkflowNativeSelectRef(field.id, element)}
5500
+ onFocus={() => {
5501
+ updateTopRoute((route) => route.kind === "workflow"
5502
+ ? { ...route, activeFieldId: field.id, error: null }
5503
+ : route);
5504
+ }}
5505
+ onChange={(nextValue) => updateWorkflowValue(field.id, nextValue)}
5506
+ />
5086
5507
  ) : (
5087
- <box
5508
+ <Box
5088
5509
  height={1}
5089
- backgroundColor={borderColor}
5510
+ backgroundColor={nativePaneChrome ? "transparent" : borderColor}
5090
5511
  onMouseDown={(event: any) => {
5091
5512
  event.stopPropagation?.();
5092
5513
  openWorkflowFieldPicker(currentRoute, field);
5093
5514
  }}
5515
+ style={nativePaneChrome ? { borderRadius: 4 } : undefined}
5094
5516
  >
5095
- <text fg={active ? paletteText : paletteSubtleText}>
5096
- {truncateText(summarizeWorkflowFieldValue(field, value), barWidth - contentPadding * 2)}
5097
- </text>
5098
- </box>
5517
+ <Text fg={active ? paletteText : paletteSubtleText}>
5518
+ {truncateText(summarizeWorkflowFieldValue(field, value), queryDisplayWidth)}
5519
+ </Text>
5520
+ </Box>
5099
5521
  )}
5100
- {field.description && (
5101
- <box height={1}>
5102
- <text fg={paletteSubtleText}>
5103
- {truncateText(
5104
- field.type === "textarea" && active
5105
- ? `${field.description} Ctrl+S submits.`
5106
- : field.description,
5107
- barWidth - contentPadding * 2,
5108
- )}
5109
- </text>
5110
- </box>
5522
+ {fieldDescription && (
5523
+ <Box height={1}>
5524
+ <Text fg={paletteSubtleText}>
5525
+ {truncateText(fieldDescription, queryDisplayWidth)}
5526
+ </Text>
5527
+ </Box>
5111
5528
  )}
5112
- </box>
5529
+ </Box>
5113
5530
  );
5114
5531
  })}
5115
5532
  {currentRoute.error && (
5116
- <box height={1}>
5117
- <text fg={colors.negative}>{truncateText(currentRoute.error, barWidth - contentPadding * 2)}</text>
5118
- </box>
5533
+ <Box height={1}>
5534
+ <Text fg={colors.negative}>{truncateText(currentRoute.error, queryDisplayWidth)}</Text>
5535
+ </Box>
5119
5536
  )}
5120
5537
  {currentRoute.pendingLabel && currentRoute.pending && (
5121
- <box height={1}>
5538
+ <Box height={1}>
5122
5539
  <Spinner label={currentRoute.pendingLabel} />
5123
- </box>
5540
+ </Box>
5124
5541
  )}
5125
- <box flexGrow={1} />
5126
- <box flexDirection="row" gap={1} justifyContent={visibleFields.some((field) => field.type === "textarea") ? "flex-end" : "flex-start"}>
5542
+ {!nativePaneChrome && <Box flexGrow={1} />}
5543
+ <Box flexDirection="row" gap={1} justifyContent={visibleFields.some((field) => field.type === "textarea") ? "flex-end" : "flex-start"}>
5127
5544
  <Button label={currentRoute.submitLabel} variant="primary" onPress={() => { void submitWorkflowRoute(currentRoute); }} disabled={currentRoute.pending} />
5128
- </box>
5129
- </box>
5545
+ </Box>
5546
+ </>
5547
+ );
5548
+
5549
+ if (nativePaneChrome) {
5550
+ return (
5551
+ <ScrollBox
5552
+ ref={workflowScrollRef}
5553
+ height={bodyHeight}
5554
+ scrollY
5555
+ style={{ overflowX: "hidden", paddingRight: 4 }}
5556
+ >
5557
+ <Box
5558
+ flexDirection="column"
5559
+ paddingX={contentPadding}
5560
+ >
5561
+ {workflowContent}
5562
+ </Box>
5563
+ </ScrollBox>
5564
+ );
5565
+ }
5566
+
5567
+ return (
5568
+ <Box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5569
+ {workflowContent}
5570
+ </Box>
5130
5571
  );
5131
5572
  };
5132
5573
 
5133
5574
  const renderConfirmBody = () => {
5134
5575
  if (currentRoute?.kind !== "confirm") return null;
5135
5576
  return (
5136
- <box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5577
+ <Box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5137
5578
  {currentRoute.body.map((line, index) => (
5138
- <box key={`confirm:${index}`} height={1}>
5139
- <text fg={paletteText}>{truncateText(line, barWidth - contentPadding * 2)}</text>
5140
- </box>
5579
+ <Box key={`confirm:${index}`} height={1}>
5580
+ <Text fg={paletteText}>{truncateText(line, queryDisplayWidth)}</Text>
5581
+ </Box>
5141
5582
  ))}
5142
- <box height={1} />
5583
+ <Box height={1} />
5143
5584
  {currentRoute.error && (
5144
- <box height={1}>
5145
- <text fg={colors.negative}>{truncateText(currentRoute.error, barWidth - contentPadding * 2)}</text>
5146
- </box>
5585
+ <Box height={1}>
5586
+ <Text fg={colors.negative}>{truncateText(currentRoute.error, queryDisplayWidth)}</Text>
5587
+ </Box>
5147
5588
  )}
5148
5589
  {currentRoute.pending && (
5149
- <box height={1}>
5590
+ <Box height={1}>
5150
5591
  <Spinner label="Working…" />
5151
- </box>
5592
+ </Box>
5152
5593
  )}
5153
- <box flexGrow={1} />
5154
- <box flexDirection="row" gap={1}>
5594
+ <Box flexGrow={1} />
5595
+ <Box flexDirection="row" gap={1}>
5155
5596
  <Button
5156
5597
  label={currentRoute.confirmLabel}
5157
5598
  variant={currentRoute.tone === "danger" ? "danger" : "primary"}
5158
5599
  onPress={() => { void confirmCurrentRoute(); }}
5159
5600
  disabled={currentRoute.pending}
5160
5601
  />
5161
- </box>
5162
- </box>
5602
+ </Box>
5603
+ </Box>
5163
5604
  );
5164
5605
  };
5165
5606
 
@@ -5177,7 +5618,7 @@ export function CommandBar({
5177
5618
  ? 0
5178
5619
  : Math.max(0, Math.min(currentRoute.selectedIdx, items.length - 1));
5179
5620
  return (
5180
- <box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5621
+ <Box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
5181
5622
  <ToggleList
5182
5623
  items={items}
5183
5624
  selectedIdx={selectedIdx}
@@ -5190,113 +5631,149 @@ export function CommandBar({
5190
5631
  : route);
5191
5632
  }}
5192
5633
  onToggle={(id) => handleMultiSelectToggle(id)}
5193
- bgColor={paletteBg}
5634
+ bgColor={nativePaneChrome ? panelBg : paletteBg}
5194
5635
  />
5195
- <box flexDirection="row" gap={1}>
5636
+ <Box flexDirection="row" gap={1}>
5196
5637
  <Button label="Done" variant="primary" onPress={commitMultiSelectPicker} />
5197
- </box>
5198
- </box>
5638
+ </Box>
5639
+ </Box>
5199
5640
  );
5200
5641
  };
5201
5642
 
5202
5643
  return (
5203
- <box
5644
+ <Box
5204
5645
  position="absolute"
5205
5646
  top={0}
5206
5647
  left={0}
5207
5648
  width={termWidth}
5208
5649
  height={termHeight}
5209
- zIndex={100}
5650
+ zIndex={nativePaneChrome ? COMMAND_BAR_OVERLAY_Z_INDEX : 100}
5210
5651
  onMouseDown={(event: any) => {
5211
5652
  event.stopPropagation?.();
5212
5653
  event.preventDefault?.();
5213
5654
  closeAll();
5214
5655
  }}
5215
5656
  >
5216
- <box
5657
+ <Box
5217
5658
  position="absolute"
5218
5659
  top={barTop}
5219
5660
  left={barLeft}
5220
5661
  width={barWidth}
5221
5662
  height={barHeight}
5222
5663
  flexDirection="column"
5223
- backgroundColor={paletteBg}
5224
- zIndex={101}
5664
+ backgroundColor={panelBg}
5665
+ zIndex={nativePaneChrome ? COMMAND_BAR_PANEL_Z_INDEX : 101}
5225
5666
  onMouseDown={(event: any) => {
5226
5667
  event.stopPropagation?.();
5227
5668
  }}
5669
+ data-gloom-role="command-bar-panel"
5670
+ style={nativePaneChrome ? {
5671
+ borderRadius: 8,
5672
+ boxShadow: "0 14px 32px rgba(0,0,0,0.18)",
5673
+ overflow: "hidden",
5674
+ padding: `${NATIVE_COMMAND_BAR_PADDING_Y_PX}px ${NATIVE_COMMAND_BAR_PADDING_X_PX}px`,
5675
+ } : undefined}
5228
5676
  >
5229
- <box height={1} />
5677
+ {!nativePaneChrome && <Box height={1} backgroundColor={paletteBg} />}
5678
+
5679
+ {!nativePaneChrome && (
5680
+ <Box
5681
+ height={1}
5682
+ paddingX={contentPadding}
5683
+ flexDirection="row"
5684
+ alignItems="center"
5685
+ >
5686
+ {currentRoute && (
5687
+ <Box marginRight={1}>
5688
+ <Button label="Back" variant="ghost" onPress={popRoute} />
5689
+ </Box>
5690
+ )}
5691
+ <Box flexGrow={1}>
5692
+ <Text fg={paletteText} attributes={TextAttributes.BOLD}>
5693
+ {currentRoute?.kind === "mode"
5694
+ ? currentRoute.screen === "themes" ? "Change Theme"
5695
+ : currentRoute.screen === "plugins" ? "Manage Plugins"
5696
+ : currentRoute.screen === "layout" ? "Layout Actions"
5697
+ : "Security Description"
5698
+ : currentRoute?.kind === "picker" ? currentRoute.title
5699
+ : currentRoute?.kind === "pane-settings" ? "Pane Settings"
5700
+ : currentRoute?.kind === "workflow" ? currentRoute.title
5701
+ : currentRoute?.kind === "confirm" ? currentRoute.title
5702
+ : "Commands"}
5703
+ </Text>
5704
+ </Box>
5705
+ </Box>
5706
+ )}
5230
5707
 
5231
- <box height={1} paddingX={contentPadding} flexDirection="row" alignItems="center">
5232
- {currentRoute && (
5233
- <box marginRight={1}>
5234
- <Button label="Back" variant="ghost" onPress={popRoute} />
5235
- </box>
5708
+ <Box key={bodySlotKey} flexDirection="column" flexGrow={1} width="100%" backgroundColor={panelBg}>
5709
+ {nativePaneChrome && currentRoute && (
5710
+ <Box height={1} paddingX={contentPadding}>
5711
+ <Text
5712
+ fg={paletteSubtleText}
5713
+ onMouseDown={(event: any) => {
5714
+ event.stopPropagation?.();
5715
+ event.preventDefault?.();
5716
+ popRoute();
5717
+ }}
5718
+ data-gloom-interactive="true"
5719
+ >
5720
+ ← Back
5721
+ </Text>
5722
+ </Box>
5236
5723
  )}
5237
- <box flexGrow={1}>
5238
- <text fg={paletteText} attributes={TextAttributes.BOLD}>
5239
- {currentRoute?.kind === "mode"
5240
- ? currentRoute.screen === "themes" ? "Change Theme"
5241
- : currentRoute.screen === "plugins" ? "Manage Plugins"
5242
- : currentRoute.screen === "layout" ? "Layout Actions"
5243
- : currentRoute.screen === "new-pane" ? "New Pane"
5244
- : "Search Ticker"
5245
- : currentRoute?.kind === "picker" ? currentRoute.title
5246
- : currentRoute?.kind === "pane-settings" ? "Pane Settings"
5247
- : currentRoute?.kind === "workflow" ? currentRoute.title
5248
- : currentRoute?.kind === "confirm" ? currentRoute.title
5249
- : "Commands"}
5250
- </text>
5251
- </box>
5252
- </box>
5253
-
5254
- <box key={bodySlotKey} flexDirection="column" flexGrow={1} width="100%" backgroundColor={paletteBg}>
5255
5724
  {(visibleListState || currentRoute?.kind === "picker") && visibleListState && (
5256
5725
  <>
5257
- <box height={1} paddingX={contentPadding}>
5258
- <box width={queryDisplayWidth} height={1} position="relative">
5259
- <input
5726
+ <Box height={1} paddingX={contentPadding}>
5727
+ <Box
5728
+ width={queryDisplayWidth}
5729
+ height={1}
5730
+ position="relative"
5731
+ backgroundColor={nativePaneChrome ? undefined : inputBg}
5732
+ style={nativePaneChrome ? undefined : {
5733
+ overflow: "hidden",
5734
+ }}
5735
+ >
5736
+ <Input
5260
5737
  value={visibleListState.query}
5261
5738
  onInput={setActiveListQuery}
5262
5739
  onChange={setActiveListQuery}
5263
5740
  placeholder={visibleListState.kind === "root" ? "Search" : "Filter"}
5264
5741
  focused
5265
- width={queryDisplayWidth}
5266
- backgroundColor={paletteBg}
5267
- focusedBackgroundColor={paletteBg}
5742
+ width={nativePaneChrome ? "100%" : queryDisplayWidth}
5743
+ backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
5744
+ focusedBackgroundColor={nativePaneChrome ? "transparent" : paletteBg}
5268
5745
  textColor={paletteText}
5269
5746
  focusedTextColor={paletteText}
5270
5747
  placeholderColor={paletteSubtleText}
5271
5748
  cursorColor={colors.textBright}
5272
5749
  />
5273
5750
  {visibleListState.kind === "root" && rootGhostSuffix && (
5274
- <box
5751
+ <Box
5275
5752
  position="absolute"
5276
5753
  top={0}
5277
5754
  left={Math.max(0, Math.min(rootQuery.length, queryDisplayWidth - 1))}
5278
5755
  width={Math.max(0, queryDisplayWidth - Math.min(rootQuery.length, queryDisplayWidth - 1))}
5279
5756
  height={1}
5280
5757
  >
5281
- <text fg={paletteSubtleText}>
5758
+ <Text fg={paletteSubtleText}>
5282
5759
  {truncateText(
5283
5760
  rootGhostSuffix,
5284
5761
  Math.max(0, queryDisplayWidth - Math.min(rootQuery.length, queryDisplayWidth - 1)),
5285
5762
  )}
5286
- </text>
5287
- </box>
5763
+ </Text>
5764
+ </Box>
5288
5765
  )}
5289
- </box>
5290
- </box>
5291
- <box height={1} paddingX={contentPadding}>
5766
+ </Box>
5767
+ </Box>
5768
+ <Box height={1} paddingX={contentPadding}>
5292
5769
  {visibleListState.kind === "root" && rootShortcutFeedback
5293
5770
  ? (
5294
- <text fg={paletteSubtleText}>
5295
- {truncateText(rootShortcutFeedback, barWidth - contentPadding * 2)}
5296
- </text>
5771
+ <Text fg={paletteSubtleText}>
5772
+ {truncateText(rootShortcutFeedback, queryDisplayWidth)}
5773
+ </Text>
5297
5774
  )
5298
5775
  : null}
5299
- </box>
5776
+ </Box>
5300
5777
  </>
5301
5778
  )}
5302
5779
 
@@ -5304,10 +5781,10 @@ export function CommandBar({
5304
5781
  {currentRoute?.kind === "workflow" && renderWorkflowBody()}
5305
5782
  {currentRoute?.kind === "confirm" && renderConfirmBody()}
5306
5783
  {showCustomMultiSelectPicker && renderMultiSelectBody()}
5307
- </box>
5784
+ </Box>
5308
5785
 
5309
- <box flexGrow={1} />
5310
- </box>
5311
- </box>
5786
+ {!nativePaneChrome && <Box flexGrow={1} />}
5787
+ </Box>
5788
+ </Box>
5312
5789
  );
5313
5790
  }