gloomberb 0.7.4 → 0.8.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 (1293) hide show
  1. package/README.md +27 -6
  2. package/package.json +2 -3
  3. package/src/api-client/auth.ts +167 -0
  4. package/src/api-client/chat.ts +116 -0
  5. package/src/api-client/data.ts +177 -0
  6. package/src/api-client/errors.ts +28 -0
  7. package/src/api-client/index.test.ts +675 -0
  8. package/src/api-client/index.ts +384 -0
  9. package/src/api-client/normalizers.ts +61 -0
  10. package/src/api-client/paths.ts +173 -0
  11. package/src/api-client/request.ts +124 -0
  12. package/src/api-client/socket.ts +483 -0
  13. package/src/api-client/types.ts +490 -0
  14. package/src/app/app-bootstrap-state.test.ts +157 -0
  15. package/src/app/app-bootstrap-state.ts +134 -0
  16. package/src/app/global-shortcuts.test.tsx +166 -0
  17. package/src/app/global-shortcuts.ts +126 -0
  18. package/src/app/pane-runtime/index.tsx +297 -0
  19. package/src/app/pane-runtime/layout-placement.ts +73 -0
  20. package/src/app/pane-runtime/pane-settings-runtime.tsx +67 -0
  21. package/src/app/pane-runtime/pane-template-runtime.ts +110 -0
  22. package/src/app/pane-runtime/plugin-bindings.ts +231 -0
  23. package/src/app/pane-runtime/ticker-inspector-runtime.ts +167 -0
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +144 -0
  25. package/src/app/pane-template-dialog-wizard.tsx +52 -0
  26. package/src/app/runtime/broker-import.ts +109 -0
  27. package/src/app/runtime/desktop-menu.ts +74 -0
  28. package/src/app/runtime/plugin-bindings.ts +246 -0
  29. package/src/app/runtime/startup.ts +126 -0
  30. package/src/app/runtime/ticker-refresh.ts +249 -0
  31. package/src/app/runtime/update.ts +86 -0
  32. package/src/app.tsx +110 -1461
  33. package/src/brokers/broker-portfolio-sync.ts +186 -0
  34. package/src/brokers/broker-ticker-sync.ts +141 -0
  35. package/src/brokers/profile-form.ts +2 -2
  36. package/src/brokers/remote-broker-adapter.ts +3 -2
  37. package/src/brokers/sync-broker-instance.test.ts +140 -0
  38. package/src/brokers/sync-broker-instance.ts +60 -181
  39. package/src/capabilities/types.ts +4 -4
  40. package/src/cli/commands/search.ts +1 -1
  41. package/src/cli/commands/ticker.ts +3 -3
  42. package/src/cli/context.ts +1 -1
  43. package/src/cli/desktop-pane-shot.ts +305 -0
  44. package/src/cli/helpers.ts +1 -1
  45. package/src/cli/index.ts +34 -0
  46. package/src/cli/pane-functions/catalog.ts +243 -0
  47. package/src/cli/pane-functions/data.ts +78 -0
  48. package/src/cli/pane-functions/discovery.ts +113 -0
  49. package/src/cli/pane-functions/index.test.ts +114 -0
  50. package/src/cli/pane-functions/index.ts +112 -0
  51. package/src/cli/pane-functions/options.ts +249 -0
  52. package/src/cli/pane-functions/report.ts +180 -0
  53. package/src/cli/pane-functions/resolver.ts +96 -0
  54. package/src/cli/pane-functions/screenshot.ts +113 -0
  55. package/src/cli/registry.ts +3 -2
  56. package/src/cli/ticker-resolution.ts +1 -1
  57. package/src/cli.test.ts +1 -1
  58. package/src/components/chart/bar-chart-renderer.test.ts +53 -0
  59. package/src/components/chart/bar-chart-renderer.ts +190 -0
  60. package/src/components/chart/comparison/data.test.ts +105 -0
  61. package/src/components/chart/comparison/data.ts +279 -0
  62. package/src/components/chart/comparison/renderer.test.ts +187 -0
  63. package/src/components/chart/comparison/renderer.ts +283 -0
  64. package/src/components/chart/comparison/scene.ts +178 -0
  65. package/src/components/chart/comparison-stock-chart/axis-width.ts +99 -0
  66. package/src/components/chart/comparison-stock-chart/canvas.ts +148 -0
  67. package/src/components/chart/comparison-stock-chart/controls.ts +252 -0
  68. package/src/components/chart/comparison-stock-chart/helpers.ts +133 -0
  69. package/src/components/chart/comparison-stock-chart/interactions.ts +202 -0
  70. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +62 -0
  71. package/src/components/chart/comparison-stock-chart/layout.tsx +155 -0
  72. package/src/components/chart/comparison-stock-chart/legend.tsx +73 -0
  73. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +89 -0
  74. package/src/components/chart/comparison-stock-chart/render-data.ts +198 -0
  75. package/src/components/chart/comparison-stock-chart/render-output.ts +145 -0
  76. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +111 -0
  77. package/src/components/chart/comparison-stock-chart/sources.ts +47 -0
  78. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +159 -0
  79. package/src/components/chart/comparison-stock-chart/toolbar.tsx +107 -0
  80. package/src/components/chart/comparison-stock-chart/types.ts +22 -0
  81. package/src/components/chart/comparison-stock-chart/view.tsx +524 -0
  82. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +239 -0
  83. package/src/components/chart/comparison-stock-chart.tsx +12 -2028
  84. package/src/components/chart/core/axis-format.ts +147 -0
  85. package/src/components/chart/core/axis-measure.ts +33 -0
  86. package/src/components/chart/core/controller.test.ts +157 -0
  87. package/src/components/chart/core/controller.ts +349 -0
  88. package/src/components/chart/core/data.ts +94 -0
  89. package/src/components/chart/core/date-window.ts +392 -0
  90. package/src/components/chart/core/index.test.ts +1007 -0
  91. package/src/components/chart/core/indicator-selector.tsx +75 -0
  92. package/src/components/chart/core/keyboard.ts +15 -0
  93. package/src/components/chart/core/palette.ts +60 -0
  94. package/src/components/chart/core/pane-settings.test.ts +25 -0
  95. package/src/components/chart/core/pane-settings.ts +82 -0
  96. package/src/components/chart/core/pointer.ts +292 -0
  97. package/src/components/chart/core/projection.ts +333 -0
  98. package/src/components/chart/core/render-utils.ts +4 -0
  99. package/src/components/chart/core/renderer.ts +249 -0
  100. package/src/components/chart/core/resolution.test.ts +99 -0
  101. package/src/components/chart/core/resolution.ts +380 -0
  102. package/src/components/chart/core/scene.ts +180 -0
  103. package/src/components/chart/core/scroll.test.ts +70 -0
  104. package/src/components/chart/core/scroll.ts +104 -0
  105. package/src/components/chart/core/time-axis.ts +330 -0
  106. package/src/components/chart/core/types.ts +124 -0
  107. package/src/components/chart/core/viewport.test.ts +29 -0
  108. package/src/components/chart/core/viewport.ts +51 -0
  109. package/src/components/chart/core/window-viewport.test.ts +69 -0
  110. package/src/components/chart/core/window-viewport.ts +162 -0
  111. package/src/components/chart/display-cursor.ts +153 -0
  112. package/src/components/chart/market-session.ts +1 -1
  113. package/src/components/chart/multi-line/renderer.test.ts +83 -0
  114. package/src/components/chart/multi-line/renderer.ts +160 -0
  115. package/src/components/chart/multi-line/scene.ts +188 -0
  116. package/src/components/chart/native/canvas-bitmaps.ts +140 -0
  117. package/src/components/chart/native/chart-rasterizer.ts +18 -865
  118. package/src/components/chart/native/kitty/adapter.ts +9 -0
  119. package/src/components/chart/native/kitty/index.test.ts +752 -0
  120. package/src/components/chart/native/kitty/manager.ts +142 -0
  121. package/src/components/chart/native/kitty/support.ts +70 -0
  122. package/src/components/chart/native/raster/comparison-chart.ts +162 -0
  123. package/src/components/chart/native/raster/crosshair.ts +51 -0
  124. package/src/components/chart/native/raster/layout.ts +136 -0
  125. package/src/components/chart/native/raster/placement.ts +132 -0
  126. package/src/components/chart/native/raster/price-chart.ts +244 -0
  127. package/src/components/chart/native/raster/primitives.ts +154 -0
  128. package/src/components/chart/native/raster/types.ts +41 -0
  129. package/src/components/chart/native/renderer-selection.ts +81 -1
  130. package/src/components/chart/native/surface/hooks.ts +137 -0
  131. package/src/components/chart/native/surface/manager.ts +225 -0
  132. package/src/components/chart/native/surface/rendering.ts +211 -0
  133. package/src/components/chart/native/surface/sync.ts +34 -0
  134. package/src/components/chart/native/surface/visibility.ts +48 -0
  135. package/src/components/chart/price-axis-labels.tsx +4 -4
  136. package/src/components/chart/rendering/chart-draw.ts +270 -0
  137. package/src/components/chart/rendering/geometry.ts +95 -0
  138. package/src/components/chart/rendering/pixel-buffer.ts +267 -0
  139. package/src/components/chart/scatter-chart-renderer.test.ts +31 -0
  140. package/src/components/chart/scatter-chart-renderer.ts +223 -0
  141. package/src/components/chart/static/bar-chart-surface.tsx +85 -0
  142. package/src/components/chart/static/chart/bitmap.ts +50 -0
  143. package/src/components/chart/static/chart/surface.test.tsx +51 -0
  144. package/src/components/chart/static/chart/surface.tsx +181 -0
  145. package/src/components/chart/static/multi-line-chart-surface.tsx +180 -0
  146. package/src/components/chart/static/scatter-chart-surface.tsx +83 -0
  147. package/src/components/chart/stock-chart/auto.ts +216 -0
  148. package/src/components/chart/stock-chart/axis-width.ts +146 -0
  149. package/src/components/chart/stock-chart/bitmaps.ts +22 -0
  150. package/src/components/chart/stock-chart/canvas.ts +202 -0
  151. package/src/components/chart/stock-chart/controls.tsx +149 -0
  152. package/src/components/chart/stock-chart/cursor.ts +60 -0
  153. package/src/components/chart/stock-chart/data-runtime.ts +129 -0
  154. package/src/components/chart/stock-chart/footer.ts +248 -0
  155. package/src/components/chart/stock-chart/geometry.ts +160 -0
  156. package/src/components/chart/stock-chart/index.test.ts +500 -0
  157. package/src/components/chart/stock-chart/index.tsx +29 -0
  158. package/src/components/chart/stock-chart/indicators.ts +226 -0
  159. package/src/components/chart/stock-chart/interaction-runtime.ts +44 -0
  160. package/src/components/chart/stock-chart/interactions.ts +300 -0
  161. package/src/components/chart/stock-chart/keyboard.ts +373 -0
  162. package/src/components/chart/stock-chart/layout-metrics.ts +62 -0
  163. package/src/components/chart/stock-chart/layout.tsx +177 -0
  164. package/src/components/chart/stock-chart/native-surfaces.ts +121 -0
  165. package/src/components/chart/stock-chart/presentation.ts +67 -0
  166. package/src/components/chart/stock-chart/renderer-switch.test.tsx +263 -0
  167. package/src/components/chart/stock-chart/rendering/bounds.ts +116 -0
  168. package/src/components/chart/stock-chart/rendering/candidates.ts +145 -0
  169. package/src/components/chart/stock-chart/rendering/data.ts +370 -0
  170. package/src/components/chart/stock-chart/rendering/invalidation.ts +68 -0
  171. package/src/components/chart/stock-chart/rendering/output.ts +304 -0
  172. package/src/components/chart/stock-chart/rendering/planning.ts +122 -0
  173. package/src/components/chart/stock-chart/rendering/projection.test.tsx +98 -0
  174. package/src/components/chart/stock-chart/rendering/projection.ts +112 -0
  175. package/src/components/chart/stock-chart/rendering/state.test.ts +70 -0
  176. package/src/components/chart/stock-chart/rendering/state.ts +283 -0
  177. package/src/components/chart/stock-chart/requests.ts +337 -0
  178. package/src/components/chart/stock-chart/resolution-support.ts +96 -0
  179. package/src/components/chart/stock-chart/resolved/runtime.ts +289 -0
  180. package/src/components/chart/stock-chart/resolved.tsx +345 -0
  181. package/src/components/chart/stock-chart/settings.ts +77 -0
  182. package/src/components/chart/stock-chart/surface-runtime.ts +62 -0
  183. package/src/components/chart/stock-chart/types.ts +25 -0
  184. package/src/components/chart/stock-chart/view.tsx +155 -0
  185. package/src/components/chart/stock-chart/viewport/effects.ts +296 -0
  186. package/src/components/chart/stock-chart/viewport/index.ts +144 -0
  187. package/src/components/chart/stock-chart/viewport/runtime.ts +375 -0
  188. package/src/components/chart/time-axis-label.tsx +4 -3
  189. package/src/components/command-bar/action-runtime.ts +326 -0
  190. package/src/components/command-bar/commands/collection.ts +214 -0
  191. package/src/components/command-bar/commands/direct/index.ts +266 -0
  192. package/src/components/command-bar/commands/direct/runtime.ts +168 -0
  193. package/src/components/command-bar/commands/plugin/actions.ts +171 -0
  194. package/src/components/command-bar/commands/plugin/items.ts +166 -0
  195. package/src/components/command-bar/commands/registry.ts +263 -0
  196. package/src/components/command-bar/helpers.test.ts +13 -21
  197. package/src/components/command-bar/helpers.ts +51 -272
  198. package/src/components/command-bar/keyboard-handlers.ts +306 -0
  199. package/src/components/command-bar/keyboard-shortcuts.ts +204 -0
  200. package/src/components/command-bar/layout-items/current-layout.ts +120 -0
  201. package/src/components/command-bar/layout-items/focused-pane.ts +143 -0
  202. package/src/components/command-bar/layout-items/types.ts +25 -0
  203. package/src/components/command-bar/layout-items/window-mode.ts +72 -0
  204. package/src/components/command-bar/layout-items.ts +93 -0
  205. package/src/components/command-bar/list/model.ts +81 -0
  206. package/src/components/command-bar/list/navigation.ts +125 -0
  207. package/src/components/command-bar/list/view.tsx +316 -0
  208. package/src/components/command-bar/multi-select-picker.tsx +219 -0
  209. package/src/components/command-bar/multi-select-runtime.ts +91 -0
  210. package/src/components/command-bar/pane-actions.ts +124 -0
  211. package/src/components/command-bar/pane-settings.ts +227 -0
  212. package/src/components/command-bar/pane-templates/items.ts +203 -0
  213. package/src/components/command-bar/pane-templates/workflow-route.ts +81 -0
  214. package/src/components/command-bar/pane-templates/workflow.ts +282 -0
  215. package/src/components/command-bar/panel/body.tsx +311 -0
  216. package/src/components/command-bar/panel/index.tsx +196 -0
  217. package/src/components/command-bar/panel/layout.ts +117 -0
  218. package/src/components/command-bar/panel/runtime.ts +280 -0
  219. package/src/components/command-bar/panel/state.ts +138 -0
  220. package/src/components/command-bar/panel/types.ts +78 -0
  221. package/src/components/command-bar/picker-activation.ts +126 -0
  222. package/src/components/command-bar/routes/root/command-items.ts +176 -0
  223. package/src/components/command-bar/routes/root/provider-search.ts +200 -0
  224. package/src/components/command-bar/routes/root/results.ts +233 -0
  225. package/src/components/command-bar/routes/root/runtime.ts +315 -0
  226. package/src/components/command-bar/routes/root/selection.ts +228 -0
  227. package/src/components/command-bar/routes/root/shortcut-feedback.ts +108 -0
  228. package/src/components/command-bar/routes/root/shortcut-items.ts +159 -0
  229. package/src/components/command-bar/routes/root/shortcuts.test.ts +151 -0
  230. package/src/components/command-bar/routes/root/shortcuts.ts +188 -0
  231. package/src/components/command-bar/routes/ticker-search/actions.ts +142 -0
  232. package/src/components/command-bar/routes/ticker-search/results.ts +94 -0
  233. package/src/components/command-bar/routes/ticker-search/route.ts +151 -0
  234. package/src/components/command-bar/routing/actions.ts +299 -0
  235. package/src/components/command-bar/routing/confirm.ts +91 -0
  236. package/src/components/command-bar/routing/effects.ts +82 -0
  237. package/src/components/command-bar/routing/launch-request.ts +59 -0
  238. package/src/components/command-bar/routing/list-state.ts +242 -0
  239. package/src/components/command-bar/routing/navigation-state.ts +147 -0
  240. package/src/components/command-bar/selection-runtime.ts +291 -0
  241. package/src/components/command-bar/surface/environment.ts +124 -0
  242. package/src/components/command-bar/surface/index.test.tsx +864 -0
  243. package/src/components/command-bar/surface/index.tsx +344 -0
  244. package/src/components/command-bar/surface/pane.test.tsx +703 -0
  245. package/src/components/command-bar/surface/portfolio.test.tsx +452 -0
  246. package/src/components/command-bar/surface/test-harness.tsx +403 -0
  247. package/src/components/command-bar/theme-preview.ts +84 -0
  248. package/src/components/command-bar/view-model.test.ts +3 -3
  249. package/src/components/command-bar/view-model.ts +7 -6
  250. package/src/components/command-bar/workflow/alert.ts +164 -0
  251. package/src/components/command-bar/workflow/body.tsx +179 -0
  252. package/src/components/command-bar/workflow/broker.ts +158 -0
  253. package/src/components/command-bar/workflow/builtin.ts +134 -0
  254. package/src/components/command-bar/workflow/collection-actions.ts +257 -0
  255. package/src/components/command-bar/workflow/coordinator.ts +193 -0
  256. package/src/components/command-bar/workflow/field-row.tsx +214 -0
  257. package/src/components/command-bar/workflow/fields.ts +230 -0
  258. package/src/components/command-bar/workflow/ops.test.ts +197 -0
  259. package/src/components/command-bar/workflow/ops.ts +268 -0
  260. package/src/components/command-bar/workflow/route-actions.ts +232 -0
  261. package/src/components/command-bar/workflow/route-builder.ts +56 -0
  262. package/src/components/command-bar/workflow/runtime.ts +234 -0
  263. package/src/components/command-bar/workflow/submit.ts +213 -0
  264. package/src/components/command-bar/workflow/tickers.ts +214 -0
  265. package/src/components/command-bar/workflow/types.ts +127 -0
  266. package/src/components/data-table/stack-view.test.tsx +218 -0
  267. package/src/components/data-table/stack-view.tsx +59 -0
  268. package/src/components/data-table/view.test.tsx +254 -0
  269. package/src/components/data-table/view.tsx +306 -0
  270. package/src/components/feed-data-table/stack-view.test.tsx +148 -0
  271. package/src/components/feed-data-table/stack-view.tsx +359 -0
  272. package/src/components/index.ts +14 -14
  273. package/src/components/input-search-bar.tsx +94 -0
  274. package/src/components/layout/detached-pane-shell.tsx +22 -16
  275. package/src/components/layout/floating-pane.tsx +19 -24
  276. package/src/components/layout/header.tsx +9 -21
  277. package/src/components/layout/pane/content.test.tsx +64 -0
  278. package/src/components/layout/pane/content.tsx +44 -0
  279. package/src/components/layout/pane/cycle-order.test.ts +30 -0
  280. package/src/components/layout/pane/cycle-order.ts +32 -0
  281. package/src/components/layout/pane/footer/index.test.tsx +131 -0
  282. package/src/components/layout/pane/footer/index.tsx +246 -0
  283. package/src/components/layout/pane/footer/model.ts +99 -0
  284. package/src/components/layout/pane/footer/registration.tsx +114 -0
  285. package/src/components/layout/pane/frame.tsx +55 -0
  286. package/src/components/layout/pane/header.test.tsx +39 -0
  287. package/src/components/layout/pane/header.tsx +252 -0
  288. package/src/components/layout/pane/index.tsx +105 -0
  289. package/src/components/layout/pane/keyboard-scroll.test.tsx +146 -0
  290. package/src/components/layout/pane/sizing.ts +66 -0
  291. package/src/components/layout/pane/title.ts +38 -0
  292. package/src/components/layout/shell/action-menu-overlay.tsx +72 -0
  293. package/src/components/layout/shell/active-drag.ts +221 -0
  294. package/src/components/layout/shell/chrome.ts +8 -0
  295. package/src/components/layout/shell/drag/index.ts +432 -0
  296. package/src/components/layout/shell/drag/overlays.tsx +78 -0
  297. package/src/components/layout/shell/drag/runtime.ts +264 -0
  298. package/src/components/layout/shell/index.test.tsx +788 -0
  299. package/src/components/layout/shell/index.tsx +451 -0
  300. package/src/components/layout/shell/layout-state.ts +97 -0
  301. package/src/components/layout/shell/menu.ts +152 -0
  302. package/src/components/layout/shell/native/pointer-runtime.ts +184 -0
  303. package/src/components/layout/shell/native/surfaces.ts +127 -0
  304. package/src/components/layout/shell/native/window-state.ts +208 -0
  305. package/src/components/layout/shell/pane/actions.ts +129 -0
  306. package/src/components/layout/shell/pane/layers.tsx +234 -0
  307. package/src/components/layout/shell/pane/management-shortcuts.ts +114 -0
  308. package/src/components/layout/shell/shortcuts.ts +47 -0
  309. package/src/components/layout/shell/terminal-pointer-runtime.ts +326 -0
  310. package/src/components/layout/shell/window-mode/index.ts +253 -0
  311. package/src/components/layout/shell/window-mode/overlays.tsx +208 -0
  312. package/src/components/layout/status-bar.test.tsx +1 -101
  313. package/src/components/layout/status-bar.tsx +1 -1
  314. package/src/components/layout/window-edit/mode.test.ts +55 -0
  315. package/src/components/layout/window-edit/mode.ts +422 -0
  316. package/src/components/layout/window-edit/presentation.ts +123 -0
  317. package/src/components/layout/window-edit/status.tsx +101 -0
  318. package/src/components/markdown-text.tsx +3 -3
  319. package/src/components/onboarding/onboarding-steps.tsx +248 -0
  320. package/src/components/onboarding/onboarding-wizard.tsx +97 -1035
  321. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +144 -0
  322. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +97 -0
  323. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +42 -0
  324. package/src/components/onboarding/portfolio-step/choose-panel.tsx +42 -0
  325. package/src/components/onboarding/portfolio-step/index.tsx +70 -0
  326. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +62 -0
  327. package/src/components/onboarding/portfolio-step/types.ts +25 -0
  328. package/src/components/onboarding/portfolio-step/utils.ts +15 -0
  329. package/src/components/onboarding/wizard-broker-sync.ts +230 -0
  330. package/src/components/onboarding/wizard-keyboard.ts +264 -0
  331. package/src/components/onboarding/wizard-model.ts +21 -0
  332. package/src/components/pane-settings-dialog/desktop.tsx +315 -0
  333. package/src/components/pane-settings-dialog/field-dialogs.tsx +213 -0
  334. package/src/components/pane-settings-dialog/tui.tsx +73 -0
  335. package/src/components/pane-settings-dialog/value.ts +36 -0
  336. package/src/components/pane-settings-dialog.tsx +18 -622
  337. package/src/components/pane-template-wizard.tsx +1 -1
  338. package/src/components/price-selector-dialog.tsx +2 -2
  339. package/src/components/price-sparkline/bitmap.ts +150 -0
  340. package/src/components/price-sparkline/index.ts +46 -0
  341. package/src/components/price-sparkline/model.ts +119 -0
  342. package/src/components/price-sparkline/view.tsx +214 -0
  343. package/src/components/quote-flash.ts +103 -0
  344. package/src/components/speedometer-gauge/desktop.tsx +151 -0
  345. package/src/components/speedometer-gauge/index.tsx +44 -0
  346. package/src/components/speedometer-gauge/model.ts +60 -0
  347. package/src/components/speedometer-gauge/terminal.tsx +261 -0
  348. package/src/components/ticker/badge/format.ts +42 -0
  349. package/src/components/ticker/badge/index.tsx +100 -0
  350. package/src/components/ticker/badge/list.tsx +57 -0
  351. package/src/components/ticker/badge/text.test.tsx +223 -0
  352. package/src/components/ticker/badge/text.tsx +156 -0
  353. package/src/components/ticker/list-table-view.tsx +297 -0
  354. package/src/components/ticker/list-table.test.tsx +177 -0
  355. package/src/components/toggle-list.tsx +1 -1
  356. package/src/components/ui/data-table/index.tsx +26 -0
  357. package/src/components/ui/data-table/opentui/index.tsx +467 -0
  358. package/src/components/ui/data-table/opentui/model.test.ts +47 -0
  359. package/src/components/ui/data-table/opentui/model.ts +66 -0
  360. package/src/components/ui/data-table/types.ts +81 -0
  361. package/src/components/ui/index.ts +1 -3
  362. package/src/components/ui/multi-select/dialog.tsx +336 -0
  363. package/src/components/ui/native-select.tsx +1 -1
  364. package/src/components/ui/shortcut-hint.tsx +1 -1
  365. package/src/components/ui/tabs.tsx +11 -5
  366. package/src/components/ui/toggle.tsx +1 -1
  367. package/src/components/ui/ui.test.tsx +3 -75
  368. package/src/components/use-external-link-footer.ts +1 -1
  369. package/src/core/app-services.ts +3 -0
  370. package/src/core/state/app/layout-reducer.ts +196 -0
  371. package/src/core/state/app/layout.ts +406 -0
  372. package/src/core/state/app/state.ts +340 -0
  373. package/src/core/state/app/types.ts +116 -0
  374. package/src/core/state/session-persistence.ts +1 -1
  375. package/src/data/app-persistence.ts +1 -1
  376. package/src/data/config/layout.ts +227 -0
  377. package/src/data/config/store/index.test.ts +500 -0
  378. package/src/data/config/store/index.ts +64 -0
  379. package/src/data/config/store/node.ts +101 -0
  380. package/src/data/config/store/normalize.ts +403 -0
  381. package/src/data/plugin-state-store.ts +3 -3
  382. package/src/data/resource-store.ts +3 -3
  383. package/src/data/session-store.ts +3 -3
  384. package/src/data/sqlite/cache.test.ts +151 -0
  385. package/src/data/sqlite/database.ts +68 -0
  386. package/src/data/sqlite/json.ts +16 -0
  387. package/src/data/sqlite/retry.test.ts +63 -0
  388. package/src/data/ticker-repository.ts +1 -1
  389. package/src/data/ticker-store.ts +2 -2
  390. package/src/market-data/coordinator/auxiliary.ts +252 -0
  391. package/src/market-data/coordinator/chart.ts +141 -0
  392. package/src/market-data/coordinator/entries.ts +129 -0
  393. package/src/market-data/coordinator/events.ts +74 -0
  394. package/src/market-data/coordinator/financials.ts +281 -0
  395. package/src/market-data/coordinator/index.test.ts +840 -0
  396. package/src/market-data/coordinator/index.ts +388 -0
  397. package/src/market-data/coordinator/quotes.ts +329 -0
  398. package/src/market-data/coordinator/subscriptions.test.ts +209 -0
  399. package/src/market-data/hooks.test.tsx +42 -1
  400. package/src/market-data/hooks.tsx +83 -18
  401. package/src/market-data/market/format.test.ts +109 -0
  402. package/src/market-data/market/format.ts +293 -0
  403. package/src/market-data/market/freshness.ts +140 -0
  404. package/src/market-data/market/status.ts +112 -0
  405. package/src/market-data/quotes/contributions.ts +194 -0
  406. package/src/market-data/quotes/freshness.test.ts +150 -0
  407. package/src/market-data/quotes/freshness.ts +36 -0
  408. package/src/market-data/quotes/resolution.test.ts +277 -0
  409. package/src/market-data/quotes/resolution.ts +468 -0
  410. package/src/market-data/quotes/time.test.ts +34 -0
  411. package/src/market-data/quotes/time.ts +59 -0
  412. package/src/market-data/request-types.ts +10 -3
  413. package/src/market-data/result-types.ts +3 -3
  414. package/src/market-data/selectors.ts +2 -2
  415. package/src/news/aggregator.ts +29 -219
  416. package/src/news/news-model.ts +208 -0
  417. package/src/news/ticker-symbols.ts +1 -1
  418. package/src/news/types.ts +3 -3
  419. package/src/plugins/builtin/account-management/footer.ts +39 -0
  420. package/src/plugins/builtin/account-management/form-components.tsx +162 -0
  421. package/src/plugins/builtin/account-management/keyboard.ts +90 -0
  422. package/src/plugins/builtin/account-management/model.ts +97 -0
  423. package/src/plugins/builtin/account-management/pane.tsx +389 -0
  424. package/src/plugins/builtin/account-management/password-dialog.tsx +125 -0
  425. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +6 -6
  426. package/src/plugins/builtin/ai/ask-ai.test.tsx +305 -0
  427. package/src/plugins/builtin/ai/index.tsx +4 -4
  428. package/src/plugins/builtin/ai/providers.ts +1 -1
  429. package/src/plugins/builtin/ai/runner.ts +1 -0
  430. package/src/plugins/builtin/ai/screener/action-bar.tsx +58 -0
  431. package/src/plugins/builtin/ai/screener/contract.test.ts +117 -0
  432. package/src/plugins/builtin/ai/screener/contract.ts +174 -0
  433. package/src/plugins/builtin/ai/screener/editor-runtime.ts +110 -0
  434. package/src/plugins/builtin/ai/screener/editor.tsx +125 -0
  435. package/src/plugins/builtin/ai/screener/footer.ts +93 -0
  436. package/src/plugins/builtin/ai/screener/keyboard.ts +90 -0
  437. package/src/plugins/builtin/ai/screener/market-runtime.ts +99 -0
  438. package/src/plugins/builtin/ai/screener/model.ts +107 -0
  439. package/src/plugins/builtin/ai/screener/pane.test.tsx +348 -0
  440. package/src/plugins/builtin/ai/screener/pane.tsx +412 -0
  441. package/src/plugins/builtin/ai/screener/results-view.tsx +169 -0
  442. package/src/plugins/builtin/ai/screener/results.ts +139 -0
  443. package/src/plugins/builtin/ai/screener/runner.ts +144 -0
  444. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +65 -0
  445. package/src/plugins/builtin/ai/settings.ts +3 -3
  446. package/src/plugins/builtin/ai/ticker-context.ts +1 -1
  447. package/src/plugins/builtin/alerts/command.ts +86 -0
  448. package/src/plugins/builtin/alerts/constants.ts +3 -0
  449. package/src/plugins/builtin/alerts/format.ts +68 -0
  450. package/src/plugins/builtin/alerts/index.test.tsx +5 -4
  451. package/src/plugins/builtin/alerts/index.tsx +18 -473
  452. package/src/plugins/builtin/alerts/pane.tsx +304 -0
  453. package/src/plugins/builtin/alerts/quotes.ts +57 -0
  454. package/src/plugins/builtin/alerts/storage.ts +20 -0
  455. package/src/plugins/builtin/analytics/broker-performance.ts +142 -0
  456. package/src/plugins/builtin/analytics/display.ts +44 -0
  457. package/src/plugins/builtin/analytics/index.test.tsx +144 -5
  458. package/src/plugins/builtin/analytics/index.tsx +108 -468
  459. package/src/plugins/builtin/analytics/metrics.ts +1 -1
  460. package/src/plugins/builtin/analytics/pane-model.ts +297 -0
  461. package/src/plugins/builtin/analytics/portfolio-selection.ts +10 -0
  462. package/src/plugins/builtin/analytics/sector-model.ts +172 -0
  463. package/src/plugins/builtin/analytics/view.tsx +207 -0
  464. package/src/plugins/builtin/broker-manager/detail.tsx +232 -0
  465. package/src/plugins/builtin/broker-manager/footer.ts +62 -0
  466. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -3
  467. package/src/plugins/builtin/broker-manager/index.tsx +125 -527
  468. package/src/plugins/builtin/broker-manager/keyboard.ts +104 -0
  469. package/src/plugins/builtin/broker-manager/model.test.ts +39 -1
  470. package/src/plugins/builtin/broker-manager/model.ts +14 -8
  471. package/src/plugins/builtin/broker-manager/pane-actions.tsx +181 -0
  472. package/src/plugins/builtin/broker-manager/table.ts +86 -0
  473. package/src/plugins/builtin/buildout/cells.tsx +203 -0
  474. package/src/plugins/builtin/buildout/data-runtime.ts +238 -0
  475. package/src/plugins/builtin/buildout/detail/company.tsx +219 -0
  476. package/src/plugins/builtin/buildout/detail/index.tsx +105 -0
  477. package/src/plugins/builtin/buildout/detail/intel.tsx +61 -0
  478. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +80 -0
  479. package/src/plugins/builtin/buildout/detail/site.tsx +142 -0
  480. package/src/plugins/builtin/buildout/detail/ui.tsx +355 -0
  481. package/src/plugins/builtin/buildout/detail/values.ts +67 -0
  482. package/src/plugins/builtin/buildout/format.ts +163 -0
  483. package/src/plugins/builtin/buildout/model/index.ts +135 -0
  484. package/src/plugins/builtin/buildout/model/normalizers.ts +396 -0
  485. package/src/plugins/builtin/buildout/model/types.ts +318 -0
  486. package/src/plugins/builtin/buildout/pane/header.tsx +55 -0
  487. package/src/plugins/builtin/buildout/pane/index.tsx +405 -0
  488. package/src/plugins/builtin/buildout/pane/status.tsx +84 -0
  489. package/src/plugins/builtin/buildout/table-model.ts +367 -0
  490. package/src/plugins/builtin/changelog/index.tsx +370 -0
  491. package/src/plugins/builtin/chat/channels.ts +157 -0
  492. package/src/plugins/builtin/chat/chat.test.tsx +1048 -0
  493. package/src/plugins/builtin/chat/composer-commands.test.ts +30 -0
  494. package/src/plugins/builtin/chat/composer-commands.ts +34 -0
  495. package/src/plugins/builtin/chat/content/channel-navigation.ts +157 -0
  496. package/src/plugins/builtin/chat/content/composer-runtime.ts +251 -0
  497. package/src/plugins/builtin/chat/content/composer.tsx +148 -0
  498. package/src/plugins/builtin/chat/content/index.tsx +407 -0
  499. package/src/plugins/builtin/chat/content/layout-metrics.ts +72 -0
  500. package/src/plugins/builtin/chat/content/scroll.ts +278 -0
  501. package/src/plugins/builtin/chat/content/selection-runtime.ts +66 -0
  502. package/src/plugins/builtin/chat/content/shortcuts.ts +212 -0
  503. package/src/plugins/builtin/chat/content/snapshot.ts +154 -0
  504. package/src/plugins/builtin/chat/content/transcript.tsx +155 -0
  505. package/src/plugins/builtin/chat/content/types.ts +26 -0
  506. package/src/plugins/builtin/chat/content/user-map.ts +21 -0
  507. package/src/plugins/builtin/chat/controller/channel-actions.ts +87 -0
  508. package/src/plugins/builtin/chat/controller/channels.ts +159 -0
  509. package/src/plugins/builtin/chat/controller/connections.ts +90 -0
  510. package/src/plugins/builtin/chat/controller/draft-sync.ts +41 -0
  511. package/src/plugins/builtin/chat/controller/fetch.ts +83 -0
  512. package/src/plugins/builtin/chat/controller/index.ts +488 -0
  513. package/src/plugins/builtin/chat/controller/lifecycle.ts +141 -0
  514. package/src/plugins/builtin/chat/controller/message-loading.ts +97 -0
  515. package/src/plugins/builtin/chat/controller/messages.ts +173 -0
  516. package/src/plugins/builtin/chat/controller/notifications.ts +56 -0
  517. package/src/plugins/builtin/chat/controller/persistence.ts +166 -0
  518. package/src/plugins/builtin/chat/controller/realtime.ts +91 -0
  519. package/src/plugins/builtin/chat/controller/send.ts +89 -0
  520. package/src/plugins/builtin/chat/controller/session-runtime.ts +156 -0
  521. package/src/plugins/builtin/chat/controller/session.ts +39 -0
  522. package/src/plugins/builtin/chat/controller/state.ts +162 -0
  523. package/src/plugins/builtin/chat/controller/storage.ts +136 -0
  524. package/src/plugins/builtin/chat/controller/utils.ts +75 -0
  525. package/src/plugins/builtin/chat/controller/view.ts +96 -0
  526. package/src/plugins/builtin/chat/controller.test.ts +1302 -0
  527. package/src/plugins/builtin/chat/index.tsx +4 -0
  528. package/src/plugins/builtin/chat/layout.ts +330 -0
  529. package/src/plugins/builtin/chat/message/action-chip.tsx +38 -0
  530. package/src/plugins/builtin/chat/message/desktop.tsx +158 -0
  531. package/src/plugins/builtin/chat/message/inline-tokens.tsx +118 -0
  532. package/src/plugins/builtin/chat/message/profile-popover.tsx +72 -0
  533. package/src/plugins/builtin/chat/message/render-state.ts +67 -0
  534. package/src/plugins/builtin/chat/message/terminal.tsx +132 -0
  535. package/src/plugins/builtin/chat/message/types.ts +18 -0
  536. package/src/plugins/builtin/chat/pane.tsx +100 -0
  537. package/src/plugins/builtin/chat/profile-popover.ts +49 -0
  538. package/src/plugins/builtin/chat/sidebar.test.tsx +455 -0
  539. package/src/plugins/builtin/chat/sidebar.tsx +296 -0
  540. package/src/plugins/builtin/chat/status-widget.tsx +103 -0
  541. package/src/plugins/builtin/chat/test-harness.tsx +212 -0
  542. package/src/plugins/builtin/cloud/auth-actions.tsx +72 -0
  543. package/src/plugins/builtin/cloud/auth-commands.ts +115 -0
  544. package/src/plugins/builtin/cloud/index.tsx +11 -0
  545. package/src/plugins/builtin/cloud/plugin.tsx +185 -0
  546. package/src/plugins/builtin/cloud-tweets/footer.ts +40 -0
  547. package/src/plugins/builtin/cloud-tweets/index.ts +1 -0
  548. package/src/plugins/builtin/cloud-tweets/keyboard.ts +65 -0
  549. package/src/plugins/builtin/cloud-tweets/model.ts +257 -0
  550. package/src/plugins/builtin/cloud-tweets/pane.tsx +318 -0
  551. package/src/plugins/builtin/cloud-tweets/registration.ts +87 -0
  552. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +49 -0
  553. package/src/plugins/builtin/cloud-tweets/table.tsx +331 -0
  554. package/src/plugins/builtin/comparison-chart/index.test.tsx +645 -0
  555. package/src/plugins/builtin/comparison-chart/index.tsx +194 -0
  556. package/src/plugins/builtin/congress-trades/detail.tsx +154 -0
  557. package/src/plugins/builtin/congress-trades/footer.ts +67 -0
  558. package/src/plugins/builtin/congress-trades/keyboard.ts +112 -0
  559. package/src/plugins/builtin/congress-trades/model.ts +224 -0
  560. package/src/plugins/builtin/congress-trades/pane.tsx +331 -0
  561. package/src/plugins/builtin/congress-trades/table.tsx +84 -0
  562. package/src/plugins/builtin/correlation/compute.ts +0 -7
  563. package/src/plugins/builtin/correlation/index.test.tsx +5 -5
  564. package/src/plugins/builtin/correlation/index.tsx +36 -208
  565. package/src/plugins/builtin/correlation/matrix/model.ts +155 -0
  566. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +56 -0
  567. package/src/plugins/builtin/correlation/relationship/controls.tsx +53 -0
  568. package/src/plugins/builtin/correlation/relationship/history.ts +72 -0
  569. package/src/plugins/builtin/correlation/relationship/model.ts +279 -0
  570. package/src/plugins/builtin/correlation/relationship/pane.tsx +359 -0
  571. package/src/plugins/builtin/correlation/relationship/view-model.ts +120 -0
  572. package/src/plugins/builtin/correlation/settings.ts +6 -6
  573. package/src/plugins/builtin/debug/index.tsx +365 -0
  574. package/src/plugins/builtin/earnings/data/cache.test.ts +94 -0
  575. package/src/plugins/builtin/earnings/data/cache.ts +145 -0
  576. package/src/plugins/builtin/earnings/data/data.test.ts +90 -0
  577. package/src/plugins/builtin/earnings/data/data.ts +115 -0
  578. package/src/plugins/builtin/earnings/index.test.ts +12 -0
  579. package/src/plugins/builtin/earnings/index.tsx +74 -266
  580. package/src/plugins/builtin/earnings/model.ts +82 -0
  581. package/src/plugins/builtin/earnings/table.ts +206 -0
  582. package/src/plugins/builtin/econ/calendar-model.test.ts +48 -0
  583. package/src/plugins/builtin/econ/calendar-model.ts +217 -0
  584. package/src/plugins/builtin/econ/calendar-source.ts +1 -1
  585. package/src/plugins/builtin/econ/detail-view.tsx +319 -0
  586. package/src/plugins/builtin/econ/fred-cache.test.ts +58 -0
  587. package/src/plugins/builtin/econ/fred-cache.ts +106 -0
  588. package/src/plugins/builtin/econ/fred-series-map.ts +0 -4
  589. package/src/plugins/builtin/econ/index.tsx +46 -474
  590. package/src/plugins/builtin/fear-greed/cache.test.ts +75 -0
  591. package/src/plugins/builtin/fear-greed/cache.ts +142 -0
  592. package/src/plugins/builtin/fear-greed/charts.tsx +305 -0
  593. package/src/plugins/builtin/fear-greed/data.test.ts +72 -0
  594. package/src/plugins/builtin/fear-greed/data.ts +370 -0
  595. package/src/plugins/builtin/fear-greed/format.ts +96 -0
  596. package/src/plugins/builtin/fear-greed/index.tsx +13 -555
  597. package/src/plugins/builtin/fear-greed/pane.tsx +169 -0
  598. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  599. package/src/plugins/builtin/fx-matrix/pairs.ts +0 -22
  600. package/src/plugins/builtin/help/components.tsx +92 -0
  601. package/src/plugins/builtin/help/index.test.tsx +167 -0
  602. package/src/plugins/builtin/help/index.tsx +256 -0
  603. package/src/plugins/builtin/help/shortcut-model.ts +145 -0
  604. package/src/plugins/builtin/holders/format.ts +44 -0
  605. package/src/plugins/builtin/holders/index.test.ts +1 -46
  606. package/src/plugins/builtin/holders/index.tsx +3 -955
  607. package/src/plugins/builtin/holders/pane.tsx +331 -0
  608. package/src/plugins/builtin/holders/table-model.ts +84 -0
  609. package/src/plugins/builtin/holders/thirteenf-match.ts +76 -0
  610. package/src/plugins/builtin/holders/treemap-layout.ts +231 -0
  611. package/src/plugins/builtin/holders/treemap.tsx +318 -0
  612. package/src/plugins/builtin/holders/types.ts +51 -0
  613. package/src/plugins/builtin/insider/index.tsx +5 -5
  614. package/src/plugins/builtin/layout-manager/index.tsx +247 -0
  615. package/src/plugins/builtin/market-movers/index.tsx +44 -282
  616. package/src/plugins/builtin/market-movers/model.ts +146 -0
  617. package/src/plugins/builtin/market-movers/screener.test.ts +31 -1
  618. package/src/plugins/builtin/market-movers/screener.ts +106 -12
  619. package/src/plugins/builtin/market-movers/table.ts +95 -0
  620. package/src/plugins/builtin/news/index.tsx +194 -0
  621. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +98 -0
  622. package/src/plugins/builtin/news/wire/breaking/notifications.ts +110 -0
  623. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +156 -0
  624. package/src/plugins/builtin/news/wire/breaking/pane.tsx +68 -0
  625. package/src/plugins/builtin/news/wire/categories.ts +114 -0
  626. package/src/plugins/builtin/news/wire/default-feeds.ts +3 -0
  627. package/src/plugins/builtin/news/wire/feed-config.test.ts +90 -0
  628. package/src/plugins/builtin/news/wire/feed-config.ts +216 -0
  629. package/src/plugins/builtin/news/wire/index.tsx +129 -0
  630. package/src/plugins/builtin/news/wire/industry-pane.tsx +126 -0
  631. package/src/plugins/builtin/news/wire/news/detail-view.tsx +306 -0
  632. package/src/plugins/builtin/news/wire/news/footer.ts +77 -0
  633. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +98 -0
  634. package/src/plugins/builtin/news/wire/news/query-presets.ts +40 -0
  635. package/src/plugins/builtin/news/wire/news/table.test.tsx +157 -0
  636. package/src/plugins/builtin/news/wire/news/table.tsx +270 -0
  637. package/src/plugins/builtin/news/wire/persisted-articles.ts +98 -0
  638. package/src/plugins/builtin/news/wire/read-state.ts +76 -0
  639. package/src/plugins/builtin/news/wire/rss/parser.test.ts +116 -0
  640. package/src/plugins/builtin/news/wire/rss/parser.ts +213 -0
  641. package/src/plugins/builtin/news/wire/rss/source.test.ts +104 -0
  642. package/src/plugins/builtin/news/wire/rss/source.ts +181 -0
  643. package/src/plugins/builtin/notes/index.tsx +49 -0
  644. package/src/plugins/builtin/notes/markdown-note-preview.tsx +39 -0
  645. package/src/plugins/builtin/notes/model.ts +36 -0
  646. package/src/plugins/builtin/notes/quick-notes-pane.tsx +334 -0
  647. package/src/plugins/builtin/notes/text-state.ts +11 -0
  648. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +112 -0
  649. package/src/plugins/builtin/options/index.tsx +44 -0
  650. package/src/plugins/builtin/options/table.ts +209 -0
  651. package/src/plugins/builtin/options/types.ts +33 -0
  652. package/src/plugins/builtin/options/view.test.tsx +185 -0
  653. package/src/plugins/builtin/options/view.tsx +264 -0
  654. package/src/plugins/builtin/plugin-groups.ts +25 -5
  655. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +50 -0
  656. package/src/plugins/builtin/portfolio-list/account-metrics.ts +46 -0
  657. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +224 -0
  658. package/src/plugins/builtin/portfolio-list/cli/render.ts +203 -0
  659. package/src/plugins/builtin/portfolio-list/cli/shared.ts +21 -0
  660. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +224 -0
  661. package/src/plugins/builtin/portfolio-list/column-values.ts +284 -0
  662. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  663. package/src/plugins/builtin/portfolio-list/header.tsx +7 -98
  664. package/src/plugins/builtin/portfolio-list/index.test.ts +133 -0
  665. package/src/plugins/builtin/portfolio-list/index.tsx +2 -1
  666. package/src/plugins/builtin/portfolio-list/metrics.test.ts +242 -0
  667. package/src/plugins/builtin/portfolio-list/metrics.ts +5 -462
  668. package/src/plugins/builtin/portfolio-list/pane/data.ts +171 -0
  669. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1267 -0
  670. package/src/plugins/builtin/portfolio-list/pane/index.tsx +385 -0
  671. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +159 -0
  672. package/src/plugins/builtin/portfolio-list/position-metrics.ts +123 -0
  673. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +326 -0
  674. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +135 -0
  675. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  676. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +126 -0
  677. package/src/plugins/builtin/portfolio-list/summary/index.tsx +392 -0
  678. package/src/plugins/builtin/portfolio-list/summary/totals.ts +86 -0
  679. package/src/plugins/builtin/portfolio-list/table.tsx +1 -1
  680. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  681. package/src/plugins/builtin/research/analyst-pane.tsx +383 -0
  682. package/src/plugins/builtin/research/corporate-actions-pane.tsx +413 -0
  683. package/src/plugins/builtin/research/index.test.ts +248 -2
  684. package/src/plugins/builtin/research/index.tsx +30 -760
  685. package/src/plugins/builtin/research/relative-valuation-pane.tsx +189 -0
  686. package/src/plugins/builtin/sec/filing-display.tsx +84 -0
  687. package/src/plugins/builtin/sec/index.tsx +342 -0
  688. package/src/plugins/builtin/sectors/index.tsx +27 -234
  689. package/src/plugins/builtin/sectors/sector-data.ts +0 -2
  690. package/src/plugins/builtin/sectors/sector-model.ts +221 -0
  691. package/src/plugins/builtin/shared/table-pane.ts +31 -0
  692. package/src/plugins/builtin/shared/ticker-request.ts +70 -0
  693. package/src/plugins/builtin/shared/ticker-surface.test.ts +57 -0
  694. package/src/plugins/builtin/shared/ticker-surface.ts +113 -0
  695. package/src/plugins/builtin/thirteenf/api.test.ts +58 -0
  696. package/src/plugins/builtin/thirteenf/api.ts +375 -0
  697. package/src/plugins/builtin/thirteenf/data.test.ts +95 -0
  698. package/src/plugins/builtin/thirteenf/data.ts +210 -0
  699. package/src/plugins/builtin/thirteenf/format.ts +55 -0
  700. package/src/plugins/builtin/thirteenf/index.tsx +80 -0
  701. package/src/plugins/builtin/thirteenf/model.test.ts +156 -0
  702. package/src/plugins/builtin/thirteenf/model.ts +618 -0
  703. package/src/plugins/builtin/thirteenf/pane.test.tsx +328 -0
  704. package/src/plugins/builtin/thirteenf/pane.tsx +826 -0
  705. package/src/plugins/builtin/thirteenf/table.ts +212 -0
  706. package/src/plugins/builtin/thirteenf/types.ts +187 -0
  707. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +91 -0
  708. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +282 -0
  709. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -5
  710. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +158 -0
  711. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +62 -0
  712. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +234 -0
  713. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +44 -0
  714. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +51 -0
  715. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +135 -0
  716. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +192 -0
  717. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +172 -0
  718. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +110 -0
  719. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +203 -0
  720. package/src/plugins/builtin/ticker-detail/financials/model.test.ts +84 -0
  721. package/src/plugins/builtin/ticker-detail/financials/model.ts +364 -0
  722. package/src/plugins/builtin/ticker-detail/financials/pane.tsx +27 -0
  723. package/src/plugins/builtin/ticker-detail/financials/schema.ts +392 -0
  724. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +438 -0
  725. package/src/plugins/builtin/ticker-detail/index.test.tsx +780 -0
  726. package/src/plugins/builtin/ticker-detail/index.tsx +174 -17
  727. package/src/plugins/builtin/ticker-detail/overview/components.tsx +240 -0
  728. package/src/plugins/builtin/ticker-detail/overview/model.ts +168 -0
  729. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +14 -0
  730. package/src/plugins/builtin/ticker-detail/overview/types.ts +17 -0
  731. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +27 -392
  732. package/src/plugins/builtin/ticker-detail/pane.tsx +24 -136
  733. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +330 -0
  734. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +292 -0
  735. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +180 -0
  736. package/src/plugins/builtin/ticker-detail/settings.ts +39 -33
  737. package/src/plugins/builtin/world-indices/footer.ts +28 -0
  738. package/src/plugins/builtin/world-indices/index.tsx +25 -218
  739. package/src/plugins/builtin/world-indices/model.ts +120 -0
  740. package/src/plugins/builtin/world-indices/table.tsx +96 -0
  741. package/src/plugins/builtin/yahoo/index.ts +72 -0
  742. package/src/plugins/builtin/yield-curve/index.tsx +5 -5
  743. package/src/plugins/builtin/yield-curve/treasury-data.ts +1 -1
  744. package/src/plugins/catalog-ui.ts +7 -9
  745. package/src/plugins/catalog.ts +1 -1
  746. package/src/plugins/ibkr/account-cache.ts +2 -10
  747. package/src/plugins/ibkr/broker-adapter.ts +10 -4
  748. package/src/plugins/ibkr/config.ts +5 -24
  749. package/src/plugins/ibkr/dialogs.tsx +0 -1
  750. package/src/plugins/ibkr/flex/index.test.ts +236 -0
  751. package/src/plugins/ibkr/flex/index.ts +337 -0
  752. package/src/plugins/ibkr/flex/statement-client.ts +181 -0
  753. package/src/plugins/ibkr/gateway/account-loaders.ts +266 -0
  754. package/src/plugins/ibkr/gateway/account-summary.ts +163 -0
  755. package/src/plugins/ibkr/gateway/client-lock.ts +154 -0
  756. package/src/plugins/ibkr/gateway/connection-events.ts +114 -0
  757. package/src/plugins/ibkr/gateway/connection.ts +152 -0
  758. package/src/plugins/ibkr/gateway/contracts.ts +132 -0
  759. package/src/plugins/ibkr/gateway/fundamentals.ts +83 -0
  760. package/src/plugins/ibkr/gateway/helpers.ts +33 -0
  761. package/src/plugins/ibkr/gateway/history-requests.ts +158 -0
  762. package/src/plugins/ibkr/gateway/history.ts +120 -0
  763. package/src/plugins/ibkr/gateway/instrument-lookup.ts +117 -0
  764. package/src/plugins/ibkr/gateway/market-data.ts +338 -0
  765. package/src/plugins/ibkr/gateway/order-execution.ts +218 -0
  766. package/src/plugins/ibkr/gateway/orders.ts +63 -0
  767. package/src/plugins/ibkr/gateway/price-normalization.test.ts +44 -0
  768. package/src/plugins/ibkr/gateway/price-normalization.ts +50 -0
  769. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +203 -0
  770. package/src/plugins/ibkr/gateway/quote-stream.ts +171 -0
  771. package/src/plugins/ibkr/gateway/selection.ts +51 -0
  772. package/src/plugins/ibkr/gateway/service/index.test.ts +614 -0
  773. package/src/plugins/ibkr/gateway/service/index.ts +432 -0
  774. package/src/plugins/ibkr/gateway/service/lifecycle.ts +191 -0
  775. package/src/plugins/ibkr/gateway/service/manager.ts +52 -0
  776. package/src/plugins/ibkr/gateway/service/market-data.ts +48 -0
  777. package/src/plugins/ibkr/gateway/service/native.ts +543 -0
  778. package/src/plugins/ibkr/gateway/timeouts.ts +13 -0
  779. package/src/plugins/ibkr/gateway/types.ts +27 -0
  780. package/src/plugins/ibkr/hash.ts +8 -0
  781. package/src/plugins/ibkr/index.test.tsx +1 -12
  782. package/src/plugins/ibkr/index.tsx +8 -9
  783. package/src/plugins/ibkr/instance-selection.test.ts +2 -2
  784. package/src/plugins/ibkr/instance-selection.ts +1 -1
  785. package/src/plugins/ibkr/portfolio-performance.test.ts +32 -0
  786. package/src/plugins/ibkr/portfolio-performance.ts +129 -0
  787. package/src/plugins/ibkr/trade/badge.tsx +28 -0
  788. package/src/plugins/ibkr/trade/dialogs.tsx +44 -0
  789. package/src/plugins/ibkr/trade/preview-panel.tsx +130 -0
  790. package/src/plugins/ibkr/trade/tab/actions.tsx +443 -0
  791. package/src/plugins/ibkr/trade/tab/field-editors.tsx +79 -0
  792. package/src/plugins/ibkr/trade/tab/footer.ts +67 -0
  793. package/src/plugins/ibkr/trade/tab/header.tsx +167 -0
  794. package/src/plugins/ibkr/trade/tab/index.test.tsx +178 -0
  795. package/src/plugins/ibkr/trade/tab/index.tsx +302 -0
  796. package/src/plugins/ibkr/trade/tab/model.ts +159 -0
  797. package/src/plugins/ibkr/trade/tab/order-request.ts +64 -0
  798. package/src/plugins/ibkr/trade/tab/shortcuts.ts +89 -0
  799. package/src/plugins/ibkr/trade/tab/state-sync.ts +86 -0
  800. package/src/plugins/ibkr/trade/ticket-panel.tsx +301 -0
  801. package/src/plugins/ibkr/trade/utils.test.ts +175 -0
  802. package/src/plugins/ibkr/trade/utils.ts +150 -0
  803. package/src/plugins/ibkr/trading/pane-view.tsx +175 -0
  804. package/src/plugins/ibkr/trading/pane.tsx +332 -0
  805. package/src/plugins/ibkr/trading/state.test.ts +109 -0
  806. package/src/plugins/ibkr/trading/state.ts +335 -0
  807. package/src/plugins/pane-manager/dock-tree.ts +282 -0
  808. package/src/plugins/pane-manager/docking.ts +202 -0
  809. package/src/plugins/pane-manager/floating-actions.ts +158 -0
  810. package/src/plugins/pane-manager/floating.ts +76 -0
  811. package/src/plugins/pane-manager/gridlock-inference.ts +126 -0
  812. package/src/plugins/pane-manager/gridlock.ts +34 -0
  813. package/src/plugins/pane-manager/layout-state.ts +160 -0
  814. package/src/plugins/pane-manager/queries.ts +75 -0
  815. package/src/plugins/pane-manager/types.ts +24 -0
  816. package/src/plugins/pane-manager.test.ts +110 -4
  817. package/src/plugins/pane-manager.ts +57 -1035
  818. package/src/plugins/prediction-markets/chart.test.tsx +2 -2
  819. package/src/plugins/prediction-markets/controller/catalog.ts +317 -0
  820. package/src/plugins/prediction-markets/controller/data.ts +171 -0
  821. package/src/plugins/prediction-markets/controller/detail.ts +243 -0
  822. package/src/plugins/prediction-markets/controller/effects.ts +253 -0
  823. package/src/plugins/prediction-markets/controller/index.ts +374 -0
  824. package/src/plugins/prediction-markets/controller/keyboard.ts +220 -0
  825. package/src/plugins/prediction-markets/controller/live-updates.ts +85 -0
  826. package/src/plugins/prediction-markets/controller/selection.ts +59 -0
  827. package/src/plugins/prediction-markets/controller/status.ts +104 -0
  828. package/src/plugins/prediction-markets/detail/outcomes.tsx +1 -1
  829. package/src/plugins/prediction-markets/metrics.ts +4 -14
  830. package/src/plugins/prediction-markets/navigation.ts +0 -13
  831. package/src/plugins/prediction-markets/pane.test.tsx +2 -198
  832. package/src/plugins/prediction-markets/plugin.test.tsx +2 -2
  833. package/src/plugins/prediction-markets/services/fetch.ts +1 -5
  834. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +299 -0
  835. package/src/plugins/prediction-markets/services/kalshi/normalize.ts +209 -0
  836. package/src/plugins/prediction-markets/services/kalshi/types.ts +89 -0
  837. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +134 -0
  838. package/src/plugins/prediction-markets/services/polymarket/detail.ts +307 -0
  839. package/src/plugins/prediction-markets/services/polymarket/normalize.ts +239 -0
  840. package/src/plugins/prediction-markets/services/polymarket/types.ts +66 -0
  841. package/src/plugins/prediction-markets/services/polymarket/ws.ts +128 -0
  842. package/src/plugins/prediction-markets/test-helpers.tsx +12 -76
  843. package/src/plugins/prediction-markets/types.ts +2 -2
  844. package/src/plugins/registry/context-menu.ts +36 -0
  845. package/src/plugins/registry/context.ts +196 -0
  846. package/src/plugins/registry/contributions.ts +174 -0
  847. package/src/plugins/registry/index.test.ts +362 -0
  848. package/src/plugins/registry/index.ts +474 -0
  849. package/src/plugins/registry/pane-settings.ts +105 -0
  850. package/src/plugins/registry/plugin-state.ts +123 -0
  851. package/src/plugins/registry/shared.ts +34 -0
  852. package/src/plugins/registry/slots.ts +70 -0
  853. package/src/plugins/runtime/context.tsx +115 -0
  854. package/src/plugins/runtime/index.test.tsx +328 -0
  855. package/src/plugins/runtime/index.tsx +111 -0
  856. package/src/plugins/runtime/state.ts +275 -0
  857. package/src/plugins/ticker-navigation.test.ts +23 -2
  858. package/src/plugins/ticker-navigation.ts +9 -3
  859. package/src/react/input.test.ts +29 -0
  860. package/src/react/input.ts +9 -0
  861. package/src/renderers/electrobun/bun/application-menu/click.test.ts +50 -0
  862. package/src/renderers/electrobun/bun/application-menu/click.ts +58 -0
  863. package/src/renderers/electrobun/bun/application-menu/index.ts +123 -0
  864. package/src/renderers/electrobun/bun/context-menu/click.test.ts +67 -0
  865. package/src/renderers/electrobun/bun/context-menu/normalize.test.ts +52 -0
  866. package/src/renderers/electrobun/bun/context-menu/normalize.ts +67 -0
  867. package/src/renderers/electrobun/bun/core-capabilities.ts +3 -0
  868. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +163 -0
  869. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +93 -0
  870. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +263 -0
  871. package/src/renderers/electrobun/bun/desktop/host-requests.ts +114 -0
  872. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +56 -0
  873. package/src/renderers/electrobun/bun/desktop/initialization.ts +139 -0
  874. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +92 -0
  875. package/src/renderers/electrobun/bun/desktop/relaunch.ts +48 -0
  876. package/src/renderers/electrobun/bun/desktop/rpc-registry.ts +43 -0
  877. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +76 -0
  878. package/src/renderers/electrobun/bun/desktop/update.ts +162 -0
  879. package/src/renderers/electrobun/bun/desktop/window-events.ts +49 -0
  880. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +63 -0
  881. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +180 -0
  882. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +97 -0
  883. package/src/renderers/electrobun/bun/index.ts +191 -947
  884. package/src/renderers/electrobun/bun/tui-entry.ts +6 -0
  885. package/src/renderers/electrobun/bun/window/focus.ts +37 -0
  886. package/src/renderers/electrobun/bun/window/frame.test.ts +40 -0
  887. package/src/renderers/electrobun/shared/protocol.ts +9 -2
  888. package/src/renderers/electrobun/view/app-services.ts +4 -375
  889. package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
  890. package/src/renderers/electrobun/view/build-assets.ts +117 -0
  891. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +398 -0
  892. package/src/renderers/electrobun/view/config-host.ts +1 -1
  893. package/src/renderers/electrobun/view/context-menu.test.ts +10 -4
  894. package/src/renderers/electrobun/view/context-menu.ts +1 -1
  895. package/src/renderers/electrobun/view/data-table/dom.ts +211 -0
  896. package/src/renderers/electrobun/view/data-table/index.tsx +358 -0
  897. package/src/renderers/electrobun/view/data-table/row.tsx +335 -0
  898. package/src/renderers/electrobun/view/desktop/control-styles.ts +77 -0
  899. package/src/renderers/electrobun/view/desktop/controls.tsx +421 -0
  900. package/src/renderers/electrobun/view/desktop/list-view.tsx +200 -0
  901. package/src/renderers/electrobun/view/desktop/window/bridge.ts +70 -0
  902. package/src/renderers/electrobun/view/desktop/window/snapshot.test.ts +57 -0
  903. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +41 -0
  904. package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
  905. package/src/renderers/electrobun/view/host/box.tsx +160 -0
  906. package/src/renderers/electrobun/view/host/chart-surface.tsx +142 -0
  907. package/src/renderers/electrobun/view/host/input.tsx +294 -0
  908. package/src/renderers/electrobun/view/host/mouse.ts +139 -0
  909. package/src/renderers/electrobun/view/host/native.ts +49 -0
  910. package/src/renderers/electrobun/view/host/scroll-box.tsx +276 -0
  911. package/src/renderers/electrobun/view/host/style.ts +124 -0
  912. package/src/renderers/electrobun/view/host/tabs.tsx +255 -0
  913. package/src/renderers/electrobun/view/host/text.tsx +121 -0
  914. package/src/renderers/electrobun/view/http-fetch.ts +1 -1
  915. package/src/renderers/electrobun/view/input-host.tsx +12 -1
  916. package/src/renderers/electrobun/view/main.tsx +45 -26
  917. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +59 -0
  918. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +9 -0
  919. package/src/renderers/electrobun/view/remote/asset-data-client.ts +201 -0
  920. package/src/renderers/electrobun/view/remote/persistence.ts +158 -0
  921. package/src/renderers/electrobun/view/remote/ticker-repository.ts +26 -0
  922. package/src/renderers/electrobun/view/styles.css +91 -4
  923. package/src/renderers/electrobun/view/ui-host.tsx +22 -1469
  924. package/src/renderers/opentui/chart-surface.tsx +4 -5
  925. package/src/renderers/opentui/host.tsx +30 -4
  926. package/src/renderers/opentui/image/loader.test.ts +25 -0
  927. package/src/renderers/opentui/image/loader.ts +105 -0
  928. package/src/renderers/opentui/image/surface.tsx +266 -0
  929. package/src/renderers/opentui/input-host.tsx +12 -1
  930. package/src/renderers/opentui/start.tsx +2 -2
  931. package/src/renderers/opentui/test-utils.tsx +2 -2
  932. package/src/renderers/opentui/ui-host.tsx +1 -1
  933. package/src/sources/gloomberb-cloud/index.test.ts +660 -0
  934. package/src/sources/gloomberb-cloud/index.ts +444 -0
  935. package/src/sources/gloomberb-cloud/news.ts +127 -0
  936. package/src/sources/gloomberb-cloud/normalizers.ts +354 -0
  937. package/src/sources/provider-errors.ts +8 -2
  938. package/src/sources/provider-router/batches.ts +170 -0
  939. package/src/sources/provider-router/brokers.ts +114 -0
  940. package/src/sources/provider-router/cache.ts +171 -0
  941. package/src/sources/provider-router/cached-financials.test.ts +498 -0
  942. package/src/sources/provider-router/document-routes.ts +153 -0
  943. package/src/sources/provider-router/financial-routes.ts +225 -0
  944. package/src/sources/provider-router/financials.ts +259 -0
  945. package/src/sources/provider-router/history.test.ts +383 -0
  946. package/src/sources/provider-router/history.ts +448 -0
  947. package/src/sources/provider-router/index.test.ts +1438 -0
  948. package/src/sources/provider-router/index.ts +389 -0
  949. package/src/sources/provider-router/news.ts +36 -0
  950. package/src/sources/provider-router/primary.ts +164 -0
  951. package/src/sources/provider-router/route-types.ts +34 -0
  952. package/src/sources/provider-router/routing.ts +58 -0
  953. package/src/sources/provider-router/search.ts +183 -0
  954. package/src/sources/provider-router/sources.ts +55 -0
  955. package/src/sources/provider-router/streaming.ts +92 -0
  956. package/src/sources/provider-router/supplemental.ts +373 -0
  957. package/src/sources/provider-router/test-support.ts +136 -0
  958. package/src/sources/sec-edgar/content.ts +211 -0
  959. package/src/sources/sec-edgar.ts +9 -217
  960. package/src/sources/yahoo-finance/financials.ts +339 -0
  961. package/src/sources/yahoo-finance/history.ts +106 -0
  962. package/src/sources/yahoo-finance/http.ts +100 -0
  963. package/src/sources/yahoo-finance/mappers.ts +409 -0
  964. package/src/sources/yahoo-finance/options.ts +134 -0
  965. package/src/sources/yahoo-finance/quote-summary.ts +210 -0
  966. package/src/sources/yahoo-finance/requests.ts +192 -0
  967. package/src/sources/yahoo-finance/snapshots.ts +236 -0
  968. package/src/sources/yahoo-finance/symbols.ts +102 -0
  969. package/src/sources/yahoo-finance/types.ts +167 -0
  970. package/src/sources/yahoo-finance.test.ts +116 -12
  971. package/src/sources/yahoo-finance.ts +115 -1408
  972. package/src/state/app/activity.test.ts +41 -0
  973. package/src/state/app/activity.ts +106 -0
  974. package/src/state/app/bootstrap.test.ts +498 -0
  975. package/src/state/app/bootstrap.ts +412 -0
  976. package/src/state/app/context/index.test.ts +564 -0
  977. package/src/state/app/context/index.test.tsx +193 -0
  978. package/src/state/app/context/index.tsx +476 -0
  979. package/src/state/app/context/selector.test.tsx +291 -0
  980. package/src/state/config-save-scheduler.ts +2 -6
  981. package/src/state/hooks/inline-tickers.test.tsx +123 -0
  982. package/src/state/hooks/inline-tickers.ts +212 -0
  983. package/src/state/hooks/quote-streaming.test.ts +31 -0
  984. package/src/state/hooks/quote-streaming.test.tsx +70 -0
  985. package/src/state/hooks/quote-streaming.ts +92 -0
  986. package/src/state/hooks/session-snapshot.ts +99 -0
  987. package/src/state/pane-scroll-registry.tsx +1 -1
  988. package/src/state/selectors-ui.ts +1 -1
  989. package/src/state/selectors.ts +1 -1
  990. package/src/test-support/plugin-persistence.ts +112 -0
  991. package/src/test-support/plugin-runtime.ts +2 -1
  992. package/src/theme/colors.ts +23 -9
  993. package/src/theme/themes.test.ts +15 -0
  994. package/src/tickers/list.test.ts +16 -0
  995. package/src/tickers/open-target.ts +77 -0
  996. package/src/tickers/search/index.test.ts +252 -0
  997. package/src/tickers/search/index.ts +367 -0
  998. package/src/tickers/search/ranking.ts +387 -0
  999. package/src/tickers/search/result.ts +27 -0
  1000. package/src/tickers/search/types.ts +41 -0
  1001. package/src/tickers/search/upsert.ts +91 -0
  1002. package/src/tickers/tokenizer.test.ts +31 -0
  1003. package/src/tickers/tokenizer.ts +37 -0
  1004. package/src/types/broker.ts +12 -4
  1005. package/src/types/config.ts +42 -23
  1006. package/src/types/context-menu.ts +1 -1
  1007. package/src/types/data-provider.ts +2 -2
  1008. package/src/types/desktop-window.ts +1 -1
  1009. package/src/types/financials.ts +109 -4
  1010. package/src/types/news-source.ts +0 -1
  1011. package/src/types/persistence.ts +1 -1
  1012. package/src/types/plugin.ts +37 -21
  1013. package/src/types/ticker.ts +1 -0
  1014. package/src/types/trading.ts +23 -1
  1015. package/src/ui/context-menu.tsx +6 -5
  1016. package/src/ui/dialog.tsx +6 -3
  1017. package/src/ui/host.tsx +10 -10
  1018. package/src/ui/index.tsx +14 -33
  1019. package/src/updater/github-releases.test.ts +41 -0
  1020. package/src/updater/github-releases.ts +73 -0
  1021. package/src/updater.test.ts +21 -0
  1022. package/src/updater.ts +10 -3
  1023. package/src/utils/back-navigation.ts +2 -2
  1024. package/src/utils/cli-output.ts +4 -4
  1025. package/src/utils/currency-units.test.ts +23 -1
  1026. package/src/utils/currency-units.ts +56 -0
  1027. package/src/utils/datetime-format.ts +40 -0
  1028. package/src/utils/dom-screenshot.ts +2 -2
  1029. package/src/utils/double-escape-close.ts +1 -1
  1030. package/src/utils/exchanges.ts +2 -2
  1031. package/src/utils/format.ts +0 -7
  1032. package/src/utils/fuzzy-search.ts +1 -1
  1033. package/src/utils/inline-content-tokenizer.test.ts +11 -2
  1034. package/src/utils/inline-content-tokenizer.ts +33 -12
  1035. package/src/utils/keyboard.ts +1 -1
  1036. package/src/utils/link-tokenizer.ts +2 -2
  1037. package/src/utils/options.ts +1 -1
  1038. package/src/utils/price-history.test.ts +20 -0
  1039. package/src/utils/price-history.ts +24 -2
  1040. package/src/utils/relative-time.ts +8 -0
  1041. package/src/utils/selection-clipboard.ts +1 -1
  1042. package/src/utils/shortcut-labels.ts +2 -2
  1043. package/src/utils/sort-values.ts +17 -0
  1044. package/src/utils/terminal-input-reset.ts +1 -1
  1045. package/src/utils/text-wrap.ts +20 -0
  1046. package/src/version.ts +1 -1
  1047. package/src/components/chart/chart-controller.test.ts +0 -110
  1048. package/src/components/chart/chart-controller.ts +0 -650
  1049. package/src/components/chart/chart-data.ts +0 -350
  1050. package/src/components/chart/chart-indicator-selector.tsx +0 -75
  1051. package/src/components/chart/chart-pane-settings.ts +0 -37
  1052. package/src/components/chart/chart-renderer.ts +0 -1562
  1053. package/src/components/chart/chart-resolution.test.ts +0 -87
  1054. package/src/components/chart/chart-resolution.ts +0 -405
  1055. package/src/components/chart/chart-scroll.test.ts +0 -70
  1056. package/src/components/chart/chart-scroll.ts +0 -104
  1057. package/src/components/chart/chart-types.ts +0 -119
  1058. package/src/components/chart/chart-viewport.test.ts +0 -29
  1059. package/src/components/chart/chart-viewport.ts +0 -50
  1060. package/src/components/chart/chart.test.ts +0 -922
  1061. package/src/components/chart/comparison-chart-data.test.ts +0 -105
  1062. package/src/components/chart/comparison-chart-data.ts +0 -292
  1063. package/src/components/chart/comparison-chart-renderer.test.ts +0 -187
  1064. package/src/components/chart/comparison-chart-renderer.ts +0 -437
  1065. package/src/components/chart/native/kitty-adapter.ts +0 -9
  1066. package/src/components/chart/native/kitty-manager.ts +0 -142
  1067. package/src/components/chart/native/kitty-support.ts +0 -71
  1068. package/src/components/chart/native/kitty.test.ts +0 -728
  1069. package/src/components/chart/native/surface-manager.ts +0 -225
  1070. package/src/components/chart/native/surface-sync.ts +0 -34
  1071. package/src/components/chart/native/surface-visibility.ts +0 -48
  1072. package/src/components/chart/static-chart-surface.test.tsx +0 -51
  1073. package/src/components/chart/static-chart-surface.tsx +0 -217
  1074. package/src/components/chart/stock-chart-renderer-switch.test.tsx +0 -1323
  1075. package/src/components/chart/stock-chart.test.ts +0 -330
  1076. package/src/components/chart/stock-chart.tsx +0 -4148
  1077. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -109
  1078. package/src/components/command-bar/command-bar.test.tsx +0 -3244
  1079. package/src/components/command-bar/command-bar.tsx +0 -5868
  1080. package/src/components/command-bar/command-registry.ts +0 -267
  1081. package/src/components/command-bar/confirm-body.tsx +0 -51
  1082. package/src/components/command-bar/list-model.ts +0 -80
  1083. package/src/components/command-bar/pane-template-display.ts +0 -8
  1084. package/src/components/command-bar/root-shortcuts.ts +0 -188
  1085. package/src/components/command-bar/workflow-ops.test.ts +0 -144
  1086. package/src/components/command-bar/workflow-ops.ts +0 -459
  1087. package/src/components/command-bar/workflow-types.ts +0 -127
  1088. package/src/components/data-table-stack-view.test.tsx +0 -241
  1089. package/src/components/data-table-stack-view.tsx +0 -59
  1090. package/src/components/data-table-view.test.tsx +0 -211
  1091. package/src/components/data-table-view.tsx +0 -244
  1092. package/src/components/feed-data-table-stack-view.test.tsx +0 -225
  1093. package/src/components/feed-data-table-stack-view.tsx +0 -359
  1094. package/src/components/layout/pane-content.test.tsx +0 -64
  1095. package/src/components/layout/pane-content.tsx +0 -44
  1096. package/src/components/layout/pane-cycle-order.test.ts +0 -30
  1097. package/src/components/layout/pane-cycle-order.ts +0 -32
  1098. package/src/components/layout/pane-footer.test.tsx +0 -195
  1099. package/src/components/layout/pane-footer.tsx +0 -432
  1100. package/src/components/layout/pane-header.test.tsx +0 -39
  1101. package/src/components/layout/pane-header.tsx +0 -248
  1102. package/src/components/layout/pane-keyboard-scroll.test.tsx +0 -226
  1103. package/src/components/layout/pane-sizing.ts +0 -34
  1104. package/src/components/layout/pane-title.ts +0 -38
  1105. package/src/components/layout/pane.tsx +0 -129
  1106. package/src/components/layout/shell.test.tsx +0 -1427
  1107. package/src/components/layout/shell.tsx +0 -1912
  1108. package/src/components/price-sparkline-view.tsx +0 -463
  1109. package/src/components/price-sparkline.ts +0 -46
  1110. package/src/components/speedometer-gauge.test.tsx +0 -45
  1111. package/src/components/speedometer-gauge.tsx +0 -562
  1112. package/src/components/ticker-badge-list.tsx +0 -53
  1113. package/src/components/ticker-badge-text.test.tsx +0 -253
  1114. package/src/components/ticker-badge-text.tsx +0 -113
  1115. package/src/components/ticker-badge.tsx +0 -109
  1116. package/src/components/ticker-list-table-view.tsx +0 -233
  1117. package/src/components/ticker-list-table.test.tsx +0 -210
  1118. package/src/components/ticker-list-table.tsx +0 -458
  1119. package/src/components/ui/data-table.tsx +0 -522
  1120. package/src/components/ui/multi-select-dialog.tsx +0 -336
  1121. package/src/core/state/app-state.ts +0 -1009
  1122. package/src/data/config-layout.ts +0 -225
  1123. package/src/data/config-store-node.ts +0 -478
  1124. package/src/data/config-store.test.ts +0 -481
  1125. package/src/data/config-store.ts +0 -64
  1126. package/src/data/sqlite-cache.test.ts +0 -151
  1127. package/src/data/sqlite-database.ts +0 -68
  1128. package/src/data/sqlite-json.ts +0 -16
  1129. package/src/data/sqlite-retry.test.ts +0 -63
  1130. package/src/market-data/coordinator-subscriptions.test.ts +0 -209
  1131. package/src/market-data/coordinator.test.ts +0 -830
  1132. package/src/market-data/coordinator.ts +0 -1155
  1133. package/src/plugins/builtin/ai/screener-contract.test.ts +0 -117
  1134. package/src/plugins/builtin/ai/screener-contract.ts +0 -174
  1135. package/src/plugins/builtin/ai/screener-pane.test.tsx +0 -348
  1136. package/src/plugins/builtin/ai/screener-pane.tsx +0 -1118
  1137. package/src/plugins/builtin/ai/utils.ts +0 -1
  1138. package/src/plugins/builtin/ask-ai.test.tsx +0 -348
  1139. package/src/plugins/builtin/chat-controller.test.ts +0 -1376
  1140. package/src/plugins/builtin/chat-controller.ts +0 -1248
  1141. package/src/plugins/builtin/chat.test.tsx +0 -2245
  1142. package/src/plugins/builtin/chat.tsx +0 -2267
  1143. package/src/plugins/builtin/cloud-auth-actions.tsx +0 -72
  1144. package/src/plugins/builtin/cloud-tweets.tsx +0 -896
  1145. package/src/plugins/builtin/comparison-chart.test.tsx +0 -532
  1146. package/src/plugins/builtin/comparison-chart.tsx +0 -194
  1147. package/src/plugins/builtin/debug.tsx +0 -365
  1148. package/src/plugins/builtin/earnings/earnings-cache.test.ts +0 -181
  1149. package/src/plugins/builtin/earnings/earnings-cache.ts +0 -145
  1150. package/src/plugins/builtin/earnings/earnings-data.test.ts +0 -90
  1151. package/src/plugins/builtin/earnings/earnings-data.ts +0 -115
  1152. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +0 -72
  1153. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +0 -366
  1154. package/src/plugins/builtin/help.test.tsx +0 -166
  1155. package/src/plugins/builtin/help.tsx +0 -450
  1156. package/src/plugins/builtin/layout-manager.tsx +0 -247
  1157. package/src/plugins/builtin/manual-entry.tsx +0 -25
  1158. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +0 -98
  1159. package/src/plugins/builtin/news-wire/breaking-notifications.ts +0 -110
  1160. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +0 -156
  1161. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -68
  1162. package/src/plugins/builtin/news-wire/categories.ts +0 -114
  1163. package/src/plugins/builtin/news-wire/default-feeds.ts +0 -3
  1164. package/src/plugins/builtin/news-wire/feed-config.test.ts +0 -90
  1165. package/src/plugins/builtin/news-wire/feed-config.ts +0 -216
  1166. package/src/plugins/builtin/news-wire/index.tsx +0 -129
  1167. package/src/plugins/builtin/news-wire/industry-pane.tsx +0 -126
  1168. package/src/plugins/builtin/news-wire/news-detail-view.tsx +0 -299
  1169. package/src/plugins/builtin/news-wire/news-footer.ts +0 -28
  1170. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +0 -98
  1171. package/src/plugins/builtin/news-wire/news-query-presets.ts +0 -40
  1172. package/src/plugins/builtin/news-wire/news-table.test.tsx +0 -206
  1173. package/src/plugins/builtin/news-wire/news-table.tsx +0 -296
  1174. package/src/plugins/builtin/news-wire/persisted-articles.ts +0 -98
  1175. package/src/plugins/builtin/news-wire/read-state.ts +0 -76
  1176. package/src/plugins/builtin/news-wire/rss-parser.test.ts +0 -184
  1177. package/src/plugins/builtin/news-wire/rss-parser.ts +0 -213
  1178. package/src/plugins/builtin/news-wire/rss-source.test.ts +0 -159
  1179. package/src/plugins/builtin/news-wire/rss-source.ts +0 -181
  1180. package/src/plugins/builtin/news.tsx +0 -194
  1181. package/src/plugins/builtin/notes.tsx +0 -561
  1182. package/src/plugins/builtin/options.test.tsx +0 -241
  1183. package/src/plugins/builtin/options.tsx +0 -542
  1184. package/src/plugins/builtin/portfolio-list/cli/commands.ts +0 -657
  1185. package/src/plugins/builtin/portfolio-list/pane.tsx +0 -724
  1186. package/src/plugins/builtin/portfolio-list/quick-add.tsx +0 -422
  1187. package/src/plugins/builtin/portfolio-list/summary.tsx +0 -275
  1188. package/src/plugins/builtin/portfolio-list.test.ts +0 -133
  1189. package/src/plugins/builtin/portfolio-list.test.tsx +0 -1535
  1190. package/src/plugins/builtin/portfolio-metrics.test.ts +0 -236
  1191. package/src/plugins/builtin/quote-monitor.test.tsx +0 -222
  1192. package/src/plugins/builtin/sec-filing-display.tsx +0 -84
  1193. package/src/plugins/builtin/sec.tsx +0 -342
  1194. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +0 -461
  1195. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +0 -479
  1196. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +0 -269
  1197. package/src/plugins/builtin/ticker-detail.test.tsx +0 -888
  1198. package/src/plugins/builtin/ticker-surface.ts +0 -68
  1199. package/src/plugins/builtin/yahoo.ts +0 -72
  1200. package/src/plugins/catalog-ui.test.ts +0 -24
  1201. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +0 -35
  1202. package/src/plugins/ibkr/flex.test.ts +0 -162
  1203. package/src/plugins/ibkr/flex.ts +0 -371
  1204. package/src/plugins/ibkr/gateway-helpers.ts +0 -33
  1205. package/src/plugins/ibkr/gateway-selection.ts +0 -51
  1206. package/src/plugins/ibkr/gateway-service-native.ts +0 -2377
  1207. package/src/plugins/ibkr/gateway-service.test.ts +0 -584
  1208. package/src/plugins/ibkr/gateway-service.ts +0 -433
  1209. package/src/plugins/ibkr/gateway-types.ts +0 -27
  1210. package/src/plugins/ibkr/price-normalization.test.ts +0 -33
  1211. package/src/plugins/ibkr/price-normalization.ts +0 -60
  1212. package/src/plugins/ibkr/trade-badge.tsx +0 -28
  1213. package/src/plugins/ibkr/trade-tab.test.tsx +0 -276
  1214. package/src/plugins/ibkr/trade-tab.tsx +0 -1117
  1215. package/src/plugins/ibkr/trade-utils.test.ts +0 -175
  1216. package/src/plugins/ibkr/trade-utils.ts +0 -150
  1217. package/src/plugins/ibkr/trading-pane.tsx +0 -474
  1218. package/src/plugins/ibkr/trading-state.test.ts +0 -109
  1219. package/src/plugins/ibkr/trading-state.ts +0 -339
  1220. package/src/plugins/plugin-runtime.test.tsx +0 -323
  1221. package/src/plugins/plugin-runtime.tsx +0 -433
  1222. package/src/plugins/prediction-markets/controller-data.ts +0 -785
  1223. package/src/plugins/prediction-markets/controller.ts +0 -659
  1224. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +0 -572
  1225. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +0 -696
  1226. package/src/plugins/prediction-markets/services/polymarket-ws.ts +0 -128
  1227. package/src/plugins/registry.test.ts +0 -362
  1228. package/src/plugins/registry.ts +0 -854
  1229. package/src/renderers/electrobun/bun/application-menu-click.test.ts +0 -50
  1230. package/src/renderers/electrobun/bun/application-menu-click.ts +0 -58
  1231. package/src/renderers/electrobun/bun/application-menu.ts +0 -123
  1232. package/src/renderers/electrobun/bun/context-menu-click.test.ts +0 -67
  1233. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +0 -52
  1234. package/src/renderers/electrobun/bun/context-menu-normalize.ts +0 -67
  1235. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +0 -63
  1236. package/src/renderers/electrobun/bun/desktop-workspace.ts +0 -180
  1237. package/src/renderers/electrobun/bun/window-focus.ts +0 -37
  1238. package/src/renderers/electrobun/bun/window-frame.test.ts +0 -40
  1239. package/src/renderers/electrobun/view/data-table.tsx +0 -810
  1240. package/src/renderers/electrobun/view/desktop-controls.tsx +0 -673
  1241. package/src/renderers/electrobun/view/desktop-window-bridge.ts +0 -70
  1242. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +0 -57
  1243. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +0 -41
  1244. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +0 -9
  1245. package/src/renderers/opentui/image-loader.test.ts +0 -25
  1246. package/src/renderers/opentui/image-loader.ts +0 -105
  1247. package/src/renderers/opentui/image-surface.tsx +0 -266
  1248. package/src/sources/gloomberb-cloud.test.ts +0 -637
  1249. package/src/sources/gloomberb-cloud.ts +0 -828
  1250. package/src/sources/provider-router.test.ts +0 -2600
  1251. package/src/sources/provider-router.ts +0 -2231
  1252. package/src/state/app-activity.test.ts +0 -41
  1253. package/src/state/app-activity.ts +0 -106
  1254. package/src/state/app-bootstrap.test.ts +0 -498
  1255. package/src/state/app-bootstrap.ts +0 -411
  1256. package/src/state/app-context-selector.test.tsx +0 -369
  1257. package/src/state/app-context.test.ts +0 -670
  1258. package/src/state/app-context.test.tsx +0 -193
  1259. package/src/state/app-context.tsx +0 -485
  1260. package/src/state/use-inline-tickers.test.tsx +0 -73
  1261. package/src/state/use-inline-tickers.ts +0 -188
  1262. package/src/state/use-quote-streaming.test.ts +0 -31
  1263. package/src/state/use-quote-streaming.test.tsx +0 -70
  1264. package/src/state/use-quote-streaming.ts +0 -92
  1265. package/src/state/use-session-snapshot.ts +0 -99
  1266. package/src/utils/api-client.test.ts +0 -603
  1267. package/src/utils/api-client.ts +0 -1379
  1268. package/src/utils/market-format.test.ts +0 -109
  1269. package/src/utils/market-format.ts +0 -299
  1270. package/src/utils/market-status.ts +0 -141
  1271. package/src/utils/quote-freshness.test.ts +0 -150
  1272. package/src/utils/quote-freshness.ts +0 -145
  1273. package/src/utils/quote-resolution.test.ts +0 -277
  1274. package/src/utils/quote-resolution.ts +0 -642
  1275. package/src/utils/quote-time.test.ts +0 -34
  1276. package/src/utils/quote-time.ts +0 -59
  1277. package/src/utils/ticker-list.test.ts +0 -16
  1278. package/src/utils/ticker-search.test.ts +0 -227
  1279. package/src/utils/ticker-search.ts +0 -855
  1280. package/src/utils/ticker-tokenizer.test.ts +0 -31
  1281. package/src/utils/ticker-tokenizer.ts +0 -37
  1282. /package/src/components/chart/native/{kitty-protocol.ts → kitty/protocol.ts} +0 -0
  1283. /package/src/components/ui/{multi-select.ts → multi-select/index.ts} +0 -0
  1284. /package/src/data/{sqlite-retry.ts → sqlite/retry.ts} +0 -0
  1285. /package/src/plugins/builtin/{news-wire → news/wire}/hash.ts +0 -0
  1286. /package/src/plugins/builtin/{news-wire → news/wire}/read-state.test.ts +0 -0
  1287. /package/src/plugins/builtin/{notes-files.ts → notes/files.ts} +0 -0
  1288. /package/src/renderers/electrobun/bun/{context-menu-click.ts → context-menu/click.ts} +0 -0
  1289. /package/src/renderers/electrobun/bun/{window-frame.ts → window/frame.ts} +0 -0
  1290. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-manager.ts → chart/surface-manager.ts} +0 -0
  1291. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-sync.ts → chart/surface-sync.ts} +0 -0
  1292. /package/src/{utils/ticker-list.ts → tickers/list.ts} +0 -0
  1293. /package/src/{utils/ticker-metadata.ts → tickers/metadata.ts} +0 -0
@@ -1,3244 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { act, useReducer } from "react";
3
- import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
4
- import { CommandBar } from "./command-bar";
5
- import { AppContext, type AppAction, type AppState, appReducer, createInitialState, getEffectiveThemeId } from "../../state/app-context";
6
- import { createTestDataProvider } from "../../test-support/data-provider";
7
- import { ThemeProvider, useThemeId } from "../../theme/theme-context";
8
- import { cloneLayout, createDefaultConfig, type AppConfig } from "../../types/config";
9
- import type { DataProvider } from "../../types/data-provider";
10
- import type { TickerRecord } from "../../types/ticker";
11
- import type { PluginRegistry } from "../../plugins/registry";
12
- import type { PaneSettingField, PaneTemplateCreateOptions } from "../../types/plugin";
13
- import { useShortcut } from "../../react/input";
14
-
15
- let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
16
-
17
- afterEach(() => {
18
- if (testSetup) {
19
- testSetup.renderer.destroy();
20
- testSetup = undefined;
21
- }
22
- });
23
-
24
- async function waitForFrameToContain(text: string, attempts = 12, delayMs = 50): Promise<string> {
25
- for (let attempt = 0; attempt < attempts; attempt++) {
26
- const frame = testSetup!.captureCharFrame();
27
- if (frame.includes(text)) {
28
- return frame;
29
- }
30
- await Bun.sleep(delayMs);
31
- await testSetup!.renderOnce();
32
- }
33
- throw new Error(`Timed out waiting for frame to contain "${text}".`);
34
- }
35
-
36
- async function clickFrameText(text: string): Promise<void> {
37
- const frame = testSetup!.captureCharFrame();
38
- const rows = frame.split("\n");
39
- const row = rows.findIndex((line) => line.includes(text));
40
- const col = row >= 0 ? rows[row]!.indexOf(text) : -1;
41
-
42
- expect(row).toBeGreaterThanOrEqual(0);
43
- expect(col).toBeGreaterThanOrEqual(0);
44
-
45
- await act(async () => {
46
- await testSetup!.mockMouse.click(col + 1, row);
47
- await testSetup!.renderOnce();
48
- });
49
- }
50
-
51
- async function emitKeypress(
52
- renderer: Awaited<ReturnType<typeof testRender>>,
53
- event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; option?: boolean },
54
- ): Promise<void> {
55
- await act(async () => {
56
- renderer.renderer.keyInput.emit("keypress", {
57
- ctrl: false,
58
- meta: false,
59
- option: false,
60
- shift: false,
61
- eventType: "press",
62
- repeated: false,
63
- stopPropagation: () => {},
64
- preventDefault: () => {},
65
- ...event,
66
- } as any);
67
- await renderer.renderOnce();
68
- });
69
- }
70
-
71
- async function renderFrames(count = 2): Promise<void> {
72
- for (let index = 0; index < count; index += 1) {
73
- await testSetup!.renderOnce();
74
- }
75
- }
76
-
77
- function expectSingleBackControl(frame: string): void {
78
- expect(frame.match(/\bBack\b/g)?.length ?? 0).toBe(1);
79
- }
80
-
81
- function makeTicker(symbol: string, name: string, overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
82
- return {
83
- metadata: {
84
- ticker: symbol,
85
- exchange: "NASDAQ",
86
- currency: "USD",
87
- name,
88
- portfolios: [],
89
- watchlists: [],
90
- positions: [],
91
- custom: {},
92
- tags: [],
93
- ...overrides,
94
- },
95
- };
96
- }
97
-
98
- function makeDataProvider(searchImpl: DataProvider["search"] = async () => []): DataProvider {
99
- return createTestDataProvider({
100
- id: "test",
101
- search: searchImpl,
102
- });
103
- }
104
-
105
- function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () => false): PluginRegistry {
106
- return {
107
- panes: new Map<string, any>([
108
- ["portfolio-list", {
109
- id: "portfolio-list",
110
- name: "Portfolio List",
111
- component: () => null,
112
- defaultPosition: "left",
113
- }],
114
- ["ticker-detail", {
115
- id: "ticker-detail",
116
- name: "Ticker Detail",
117
- component: () => null,
118
- defaultPosition: "right",
119
- }],
120
- ["chat", {
121
- id: "chat",
122
- name: "Chat",
123
- component: () => null,
124
- defaultPosition: "right",
125
- defaultMode: "floating",
126
- }],
127
- ["quote-monitor", {
128
- id: "quote-monitor",
129
- name: "Quote Monitor",
130
- component: () => null,
131
- defaultPosition: "right",
132
- defaultMode: "floating",
133
- }],
134
- ["ibkr-trading", {
135
- id: "ibkr-trading",
136
- name: "IBKR Console",
137
- component: () => null,
138
- defaultPosition: "right",
139
- defaultMode: "floating",
140
- }],
141
- ]),
142
- paneTemplates: new Map<string, any>([
143
- ["new-portfolio-pane", {
144
- id: "new-portfolio-pane",
145
- paneId: "portfolio-list",
146
- label: "New Portfolio Pane",
147
- description: "Open another portfolio list pane",
148
- shortcut: { prefix: "PF" },
149
- }],
150
- ["new-watchlist-pane", {
151
- id: "new-watchlist-pane",
152
- paneId: "portfolio-list",
153
- label: "New Watchlist Pane",
154
- description: "Open another watchlist pane",
155
- }],
156
- ["new-ticker-detail-pane", {
157
- id: "new-ticker-detail-pane",
158
- paneId: "ticker-detail",
159
- label: "New Ticker Detail Pane",
160
- description: "Open another detail pane",
161
- }],
162
- ["new-chat-pane", {
163
- id: "new-chat-pane",
164
- paneId: "chat",
165
- label: "New Chat Pane",
166
- description: "Open another floating chat window",
167
- shortcut: { prefix: "CHAT", argPlaceholder: "channel", argKind: "text" },
168
- }],
169
- ["quote-monitor-pane", {
170
- id: "quote-monitor-pane",
171
- paneId: "quote-monitor",
172
- label: "Quote Monitor",
173
- description: "Open a compact quote monitor for one or more tickers",
174
- shortcut: { prefix: "QQ", argPlaceholder: "tickers", argKind: "ticker-list" },
175
- wizard: [{ key: "tickers", label: "Quote Tickers", type: "text" }],
176
- }],
177
- ["new-ibkr-trading-pane", {
178
- id: "new-ibkr-trading-pane",
179
- paneId: "ibkr-trading",
180
- label: "New IBKR Trading Pane",
181
- description: "Open another floating IBKR trading console",
182
- shortcut: { prefix: "IBKR" },
183
- canCreate: (context: any) => context.config.brokerInstances.some((instance: any) => (
184
- instance.brokerType === "ibkr"
185
- && instance.connectionMode === "gateway"
186
- && instance.enabled !== false
187
- )),
188
- }],
189
- ]),
190
- commands: new Map<string, any>([
191
- ["plugin:scan", {
192
- id: "plugin:scan",
193
- label: "Scan Movers",
194
- description: "Run a quick movers scan",
195
- category: "plugins",
196
- execute: async () => {},
197
- }],
198
- ]),
199
- tickerActions: new Map<string, any>([
200
- ["pin", {
201
- id: "pin",
202
- label: "Pin Ticker",
203
- execute: async () => {},
204
- }],
205
- ]),
206
- brokers: new Map(),
207
- allPlugins: new Map([
208
- ["news", { id: "news", name: "News", version: "1.0.0", description: "Latest headlines", toggleable: true }],
209
- ["notes", { id: "notes", name: "Notes", version: "1.0.0", description: "Ticker notes", toggleable: true }],
210
- ]),
211
- getCommandPluginId: () => "news",
212
- getPaneTemplatePluginId: (templateId: string) => (
213
- templateId === "new-chat-pane" ? "news" : "notes"
214
- ),
215
- getPluginPaneIds: () => [],
216
- getPluginPaneTemplateIds: () => [],
217
- hasPaneSettings,
218
- events: { emit: () => {} },
219
- hidePane: () => {},
220
- pinTicker: () => {},
221
- showPane: () => {},
222
- updateLayoutFn: () => {},
223
- getTermSizeFn: () => ({ width: 80, height: 24 }),
224
- notify: () => {},
225
- createPaneFromTemplateFn: () => {},
226
- createPaneFromTemplateAsyncFn: async () => {},
227
- openPaneSettingsFn: () => {},
228
- applyPaneSettingValueFn: async () => {},
229
- resolvePaneSettings: () => null,
230
- createBrokerInstanceFn: async () => { throw new Error("unused"); },
231
- syncBrokerInstanceFn: async () => {},
232
- removeBrokerInstanceFn: async () => {},
233
- getConfigFn: () => createDefaultConfig("/tmp/gloomberb-test"),
234
- } as unknown as PluginRegistry;
235
- }
236
-
237
- function makeQuoteMonitorPaneSettingsDescriptor(
238
- pluginRegistry: PluginRegistry,
239
- fields: PaneSettingField[],
240
- settings: Record<string, unknown> = {},
241
- ) {
242
- const config = createDefaultConfig("/tmp/gloomberb-test");
243
- const pane = {
244
- instanceId: "quote-monitor:main",
245
- paneId: "quote-monitor",
246
- title: "Quote Monitor",
247
- settings,
248
- };
249
- return {
250
- paneId: "quote-monitor:main",
251
- pane,
252
- paneDef: pluginRegistry.panes.get("quote-monitor")!,
253
- settingsDef: {
254
- title: "Quote Monitor Settings",
255
- fields,
256
- },
257
- context: {
258
- config,
259
- layout: cloneLayout(config.layout),
260
- paneId: "quote-monitor:main",
261
- paneType: "quote-monitor",
262
- pane,
263
- settings,
264
- paneState: {},
265
- activeTicker: "AAPL",
266
- activeCollectionId: "main",
267
- },
268
- } as any;
269
- }
270
-
271
- function ThemeProbe() {
272
- return <text>{`theme:${useThemeId()}`}</text>;
273
- }
274
-
275
- function UnhandledEnterProbe({ onEnter }: { onEnter: () => void }) {
276
- useShortcut((event) => {
277
- if (event.name === "enter" || event.name === "return") {
278
- onEnter();
279
- }
280
- });
281
- return null;
282
- }
283
-
284
- function CommandBarHarness({
285
- query,
286
- disabledPlugins = [],
287
- selectedTicker,
288
- live = false,
289
- extraTickers = [],
290
- showQueryState = false,
291
- onSaveTicker,
292
- configureConfig,
293
- configureState,
294
- configurePluginRegistry,
295
- dataProvider = makeDataProvider(),
296
- hasPaneSettings,
297
- onCheckForUpdates,
298
- onAction,
299
- onUnhandledEnter,
300
- }: {
301
- query: string;
302
- disabledPlugins?: string[];
303
- selectedTicker?: string;
304
- live?: boolean;
305
- extraTickers?: TickerRecord[];
306
- showQueryState?: boolean;
307
- onSaveTicker?: (ticker: TickerRecord) => void;
308
- configureConfig?: (config: AppConfig) => AppConfig;
309
- configureState?: (state: AppState) => AppState;
310
- configurePluginRegistry?: (pluginRegistry: PluginRegistry) => void;
311
- dataProvider?: DataProvider;
312
- hasPaneSettings?: (paneId: string) => boolean;
313
- onCheckForUpdates?: () => void | Promise<void>;
314
- onAction?: (action: AppAction) => void;
315
- onUnhandledEnter?: () => void;
316
- }) {
317
- let config = {
318
- ...createDefaultConfig("/tmp/gloomberb-test"),
319
- recentTickers: ["AAPL", "MSFT"],
320
- disabledPlugins,
321
- };
322
- if (configureConfig) {
323
- config = configureConfig(config);
324
- }
325
- const tickers = [makeTicker("AAPL", "Apple Inc."), makeTicker("MSFT", "Microsoft Corp."), ...extraTickers];
326
- let state: AppState = {
327
- ...createInitialState(config),
328
- commandBarOpen: true,
329
- commandBarQuery: query,
330
- focusedPaneId: "portfolio-list:main",
331
- tickers: new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker])),
332
- config: { ...config, disabledPlugins },
333
- paneState: selectedTicker
334
- ? {
335
- "portfolio-list:main": {
336
- collectionId: "main",
337
- cursorSymbol: selectedTicker,
338
- },
339
- }
340
- : createInitialState(config).paneState,
341
- };
342
- if (configureState) {
343
- state = configureState(state);
344
- }
345
- const tickerRepository = {
346
- loadTicker: async () => null,
347
- createTicker: async (metadata: TickerRecord["metadata"]) => ({
348
- metadata,
349
- }),
350
- saveTicker: async (ticker: TickerRecord) => {
351
- onSaveTicker?.(ticker);
352
- },
353
- };
354
- const pluginRegistry = makePluginRegistry(hasPaneSettings);
355
- configurePluginRegistry?.(pluginRegistry);
356
- const [liveState, dispatch] = useReducer(appReducer, state);
357
- const currentState = live ? liveState : state;
358
- const currentDispatch = live
359
- ? (action: AppAction) => {
360
- onAction?.(action);
361
- dispatch(action);
362
- }
363
- : (_action: AppAction) => {};
364
-
365
- return (
366
- <ThemeProvider themeId={getEffectiveThemeId(currentState)}>
367
- <AppContext value={{ state: currentState, dispatch: currentDispatch }}>
368
- <TestDialogProvider>
369
- {live && <ThemeProbe />}
370
- {showQueryState && <text>{`query:${currentState.commandBarQuery}`}</text>}
371
- {currentState.commandBarOpen && (
372
- <CommandBar
373
- dataProvider={dataProvider}
374
- tickerRepository={tickerRepository as any}
375
- pluginRegistry={pluginRegistry}
376
- quitApp={() => {}}
377
- onCheckForUpdates={onCheckForUpdates}
378
- />
379
- )}
380
- {onUnhandledEnter && <UnhandledEnterProbe onEnter={onUnhandledEnter} />}
381
- </TestDialogProvider>
382
- </AppContext>
383
- </ThemeProvider>
384
- );
385
- }
386
-
387
- describe("CommandBar", () => {
388
- test("renders the default layout with opencode-style chrome", async () => {
389
- testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
390
- width: 80,
391
- height: 24,
392
- });
393
-
394
- await testSetup.renderOnce();
395
-
396
- expect(testSetup.captureCharFrame()).toMatchSnapshot();
397
- });
398
-
399
- test("runs check for updates from the command bar", async () => {
400
- const calls: number[] = [];
401
-
402
- testSetup = await testRender(<CommandBarHarness query="check for updates" live onCheckForUpdates={() => { calls.push(Date.now()); }} />, {
403
- width: 80,
404
- height: 24,
405
- });
406
-
407
- await testSetup.renderOnce();
408
- expect(testSetup.captureCharFrame()).toContain("Check for Updates");
409
-
410
- await clickFrameText("Check for Updates");
411
- await Bun.sleep(0);
412
- await testSetup.renderOnce();
413
-
414
- expect(calls).toHaveLength(1);
415
- expect(testSetup.captureCharFrame()).not.toContain("Commands");
416
- });
417
-
418
- test("runs plugin command shortcuts from the root query", async () => {
419
- const calls: string[] = [];
420
-
421
- testSetup = await testRender(<CommandBarHarness
422
- query="GL"
423
- configurePluginRegistry={(pluginRegistry) => {
424
- (pluginRegistry.commands as Map<string, any>).set("gridlock-all", {
425
- id: "gridlock-all",
426
- label: "Gridlock All Windows",
427
- description: "Arrange all visible panes into a tiled grid",
428
- keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
429
- shortcut: "GL",
430
- category: "config",
431
- execute: async () => {
432
- calls.push("gridlock-all");
433
- },
434
- });
435
- (pluginRegistry.allPlugins as Map<string, any>).set("layout-manager", {
436
- id: "layout-manager",
437
- name: "Layout Manager",
438
- version: "1.0.0",
439
- description: "Pane layout management commands",
440
- });
441
- const getCommandPluginId = pluginRegistry.getCommandPluginId;
442
- pluginRegistry.getCommandPluginId = (commandId: string) => (
443
- commandId === "gridlock-all" ? "layout-manager" : getCommandPluginId(commandId)
444
- );
445
- }}
446
- />, {
447
- width: 80,
448
- height: 24,
449
- });
450
-
451
- await testSetup.renderOnce();
452
-
453
- const frame = testSetup.captureCharFrame();
454
- expect(frame).toContain("Gridlock All Windows");
455
- expect(frame).toContain("GL");
456
-
457
- await act(async () => {
458
- testSetup!.mockInput.pressEnter();
459
- await Bun.sleep(0);
460
- await testSetup!.renderOnce();
461
- });
462
-
463
- expect(calls).toEqual(["gridlock-all"]);
464
- });
465
-
466
- test("surfaces plugin commands by add-style search terms", async () => {
467
- testSetup = await testRender(<CommandBarHarness
468
- query="add alert"
469
- configurePluginRegistry={(pluginRegistry) => {
470
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
471
- id: "set-alert",
472
- label: "Add Alert",
473
- description: "Create a price alert from a symbol, condition, and target price",
474
- keywords: ["add", "set", "alert", "price", "trigger"],
475
- shortcut: "SA",
476
- category: "data",
477
- execute: async () => {},
478
- });
479
- }}
480
- />, {
481
- width: 80,
482
- height: 24,
483
- });
484
-
485
- await testSetup.renderOnce();
486
-
487
- const frame = testSetup.captureCharFrame();
488
- expect(frame).toContain("Add Alert");
489
- expect(frame).toContain("SA");
490
- });
491
-
492
- test("opens plugin command shortcut arguments in the wizard for confirmation", async () => {
493
- const calls: Array<Record<string, string> | undefined> = [];
494
-
495
- testSetup = await testRender(<CommandBarHarness
496
- query="SA AAPL above 200"
497
- configurePluginRegistry={(pluginRegistry) => {
498
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
499
- id: "set-alert",
500
- label: "Add Alert",
501
- description: "Create a price alert from a symbol, condition, and target price",
502
- keywords: ["add", "set", "alert", "price", "trigger"],
503
- shortcut: "SA",
504
- shortcutArg: {
505
- placeholder: "symbol condition price",
506
- kind: "text",
507
- parse: (arg: string) => {
508
- const [symbol, condition, price] = arg.split(/\s+/);
509
- return { symbol, condition, price };
510
- },
511
- },
512
- category: "data",
513
- wizardLayout: "form",
514
- wizard: [
515
- { key: "symbol", label: "Symbol", type: "text" },
516
- {
517
- key: "condition",
518
- label: "Condition",
519
- type: "select",
520
- options: [
521
- { label: "Above", value: "above" },
522
- { label: "Below", value: "below" },
523
- ],
524
- },
525
- { key: "price", label: "Target Price", type: "number" },
526
- ],
527
- execute: async (values?: Record<string, string>) => {
528
- calls.push(values);
529
- },
530
- });
531
- }}
532
- />, {
533
- width: 80,
534
- height: 24,
535
- });
536
-
537
- await testSetup.renderOnce();
538
-
539
- const frame = testSetup.captureCharFrame();
540
- expect(frame).toContain("Add Alert");
541
- expect(frame).toContain("AAPL above 200");
542
-
543
- await act(async () => {
544
- testSetup!.mockInput.pressEnter();
545
- await Bun.sleep(0);
546
- await testSetup!.renderOnce();
547
- });
548
-
549
- const workflowFrame = await waitForFrameToContain("Target Price");
550
- expect(workflowFrame).toContain("AAPL");
551
- expect(workflowFrame).toContain("Above");
552
- expect(workflowFrame).toContain("200");
553
- expect(calls).toEqual([]);
554
- });
555
-
556
- test("opens partial plugin command shortcut arguments in the wizard", async () => {
557
- testSetup = await testRender(<CommandBarHarness
558
- query="SA AMD"
559
- configurePluginRegistry={(pluginRegistry) => {
560
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
561
- id: "set-alert",
562
- label: "Add Alert",
563
- description: "Create a price alert from a symbol, condition, and target price",
564
- keywords: ["add", "set", "alert", "price", "trigger"],
565
- shortcut: "SA",
566
- shortcutArg: {
567
- placeholder: "symbol condition price",
568
- kind: "text",
569
- parse: (arg: string) => ({ symbol: arg.trim().toUpperCase() }),
570
- },
571
- category: "data",
572
- wizardLayout: "form",
573
- wizard: [
574
- { key: "symbol", label: "Symbol", type: "text" },
575
- {
576
- key: "condition",
577
- label: "Condition",
578
- type: "select",
579
- options: [{ label: "Above", value: "above" }],
580
- },
581
- { key: "price", label: "Target Price", type: "number" },
582
- ],
583
- execute: async () => {},
584
- });
585
- }}
586
- />, {
587
- width: 80,
588
- height: 24,
589
- });
590
-
591
- await testSetup.renderOnce();
592
-
593
- await act(async () => {
594
- testSetup!.mockInput.pressEnter();
595
- await Bun.sleep(0);
596
- await testSetup!.renderOnce();
597
- });
598
-
599
- const workflowFrame = await waitForFrameToContain("Target Price");
600
- expect(workflowFrame).toContain("AMD");
601
- });
602
-
603
- test("prefills alert command targets from the resolved quote", async () => {
604
- const quoteProvider = createTestDataProvider({
605
- id: "test",
606
- search: async () => [],
607
- getQuote: async (symbol: string) => ({
608
- symbol,
609
- price: 201.5,
610
- currency: "USD",
611
- change: 1,
612
- changePercent: 0.5,
613
- name: "Advanced Micro Devices",
614
- exchangeName: "NASDAQ",
615
- lastUpdated: Date.now(),
616
- dataSource: "live",
617
- }),
618
- });
619
-
620
- testSetup = await testRender(<CommandBarHarness
621
- query="SA AMD"
622
- dataProvider={quoteProvider}
623
- configurePluginRegistry={(pluginRegistry) => {
624
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
625
- id: "set-alert",
626
- label: "Add Alert",
627
- description: "Create a price alert from a symbol, condition, and target price",
628
- keywords: ["add", "set", "alert", "price", "trigger"],
629
- shortcut: "SA",
630
- shortcutArg: {
631
- placeholder: "symbol condition price",
632
- kind: "ticker",
633
- parse: (arg: string) => ({ symbol: arg.trim().toUpperCase() }),
634
- },
635
- category: "data",
636
- wizardLayout: "form",
637
- wizard: [
638
- { key: "symbol", label: "Symbol", type: "text" },
639
- {
640
- key: "condition",
641
- label: "Condition",
642
- type: "select",
643
- options: [{ label: "Above", value: "above" }],
644
- },
645
- { key: "price", label: "Target Price", type: "number" },
646
- ],
647
- execute: async () => {},
648
- });
649
- }}
650
- />, {
651
- width: 90,
652
- height: 24,
653
- });
654
-
655
- await testSetup.renderOnce();
656
-
657
- await act(async () => {
658
- testSetup!.mockInput.pressEnter();
659
- await Bun.sleep(0);
660
- await testSetup!.renderOnce();
661
- });
662
-
663
- const workflowFrame = await waitForFrameToContain("Advanced Micro Devices");
664
- expect(workflowFrame).toContain("201.5");
665
- });
666
-
667
- test("updates workflow select fields from the option picker", async () => {
668
- testSetup = await testRender(<CommandBarHarness
669
- query="SA AMD"
670
- configurePluginRegistry={(pluginRegistry) => {
671
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
672
- id: "set-alert",
673
- label: "Add Alert",
674
- description: "Create a price alert from a symbol, condition, and target price",
675
- keywords: ["add", "set", "alert", "price", "trigger"],
676
- shortcut: "SA",
677
- shortcutArg: {
678
- placeholder: "symbol condition price",
679
- kind: "text",
680
- parse: (arg: string) => ({ symbol: arg.trim().toUpperCase() }),
681
- },
682
- category: "data",
683
- wizardLayout: "form",
684
- wizard: [
685
- { key: "symbol", label: "Symbol", type: "text" },
686
- {
687
- key: "condition",
688
- label: "Condition",
689
- type: "select",
690
- options: [
691
- { label: "Above", value: "above" },
692
- { label: "Below", value: "below" },
693
- { label: "Crosses", value: "crosses" },
694
- ],
695
- },
696
- { key: "price", label: "Target Price", type: "number" },
697
- ],
698
- execute: async () => {},
699
- });
700
- }}
701
- />, {
702
- width: 80,
703
- height: 24,
704
- });
705
-
706
- await testSetup.renderOnce();
707
- await act(async () => {
708
- testSetup!.mockInput.pressEnter();
709
- await Bun.sleep(0);
710
- await testSetup!.renderOnce();
711
- });
712
- await waitForFrameToContain("Target Price");
713
-
714
- await act(async () => {
715
- testSetup!.mockInput.pressTab();
716
- await testSetup!.renderOnce();
717
- });
718
- await act(async () => {
719
- testSetup!.mockInput.pressEnter();
720
- await testSetup!.renderOnce();
721
- });
722
- let frame = await waitForFrameToContain("Below");
723
- expect(frame).toContain("Crosses");
724
-
725
- await act(async () => {
726
- testSetup!.mockInput.pressArrow("down");
727
- await testSetup!.renderOnce();
728
- });
729
- await act(async () => {
730
- testSetup!.mockInput.pressEnter();
731
- await testSetup!.renderOnce();
732
- });
733
-
734
- frame = await waitForFrameToContain("Target Price");
735
- expect(frame).toContain("Below");
736
- });
737
-
738
- test("opens plugin command workflows from a launch request", async () => {
739
- testSetup = await testRender(<CommandBarHarness
740
- query=""
741
- selectedTicker="AMD"
742
- extraTickers={[makeTicker("AMD", "Advanced Micro Devices")]}
743
- configureState={(state) => ({
744
- ...state,
745
- commandBarLaunchRequest: {
746
- kind: "plugin-command",
747
- commandId: "set-alert",
748
- sequence: 1,
749
- },
750
- })}
751
- configurePluginRegistry={(pluginRegistry) => {
752
- (pluginRegistry.commands as Map<string, any>).set("set-alert", {
753
- id: "set-alert",
754
- label: "Set Alert",
755
- keywords: ["alert", "price", "trigger"],
756
- shortcut: "SA",
757
- shortcutArg: {
758
- placeholder: "symbol condition price",
759
- kind: "ticker",
760
- parse: (_arg: string, context?: { activeTicker: string | null }) => (
761
- context?.activeTicker ? { symbol: context.activeTicker } : {}
762
- ),
763
- },
764
- category: "data",
765
- wizardLayout: "form",
766
- wizard: [
767
- { key: "symbol", label: "Symbol", type: "text" },
768
- {
769
- key: "condition",
770
- label: "Condition",
771
- type: "select",
772
- options: [
773
- { label: "Above", value: "above" },
774
- { label: "Below", value: "below" },
775
- ],
776
- },
777
- { key: "price", label: "Target Price", type: "number" },
778
- ],
779
- execute: async () => {},
780
- });
781
- }}
782
- />, {
783
- width: 80,
784
- height: 24,
785
- });
786
-
787
- const frame = await waitForFrameToContain("Target Price");
788
- expect(frame).toContain("Set Alert");
789
- expect(frame).toContain("Symbol");
790
- expect(frame).toContain("Condition");
791
- expect(frame).toContain("AMD");
792
- });
793
-
794
- test("renders theme prefix results in the root query", async () => {
795
- testSetup = await testRender(<CommandBarHarness query="TH " />, {
796
- width: 80,
797
- height: 24,
798
- });
799
-
800
- await testSetup.renderOnce();
801
-
802
- const frame = testSetup.captureCharFrame();
803
- expect(frame).toContain("Commands");
804
- expect(frame).toContain("TH");
805
- expect(frame).toContain("Amber");
806
- expect(frame).toContain("Paper");
807
- expect(frame).toContain("GitHub Light");
808
- expect(frame).toContain("Solarized Light");
809
- expect(frame).toContain("Gruvbox Light");
810
- expect(frame).toContain("Nord Light");
811
- });
812
-
813
- test("renders plugin toggle state", async () => {
814
- testSetup = await testRender(<CommandBarHarness query="PL " disabledPlugins={["notes"]} />, {
815
- width: 80,
816
- height: 24,
817
- });
818
-
819
- await testSetup.renderOnce();
820
-
821
- expect(testSetup.captureCharFrame()).toMatchSnapshot();
822
- });
823
-
824
- test("only surfaces PS when the focused pane exposes settings", async () => {
825
- testSetup = await testRender(
826
- <CommandBarHarness query="PS" hasPaneSettings={(paneId) => paneId === "portfolio-list:main"} />,
827
- { width: 80, height: 24 },
828
- );
829
-
830
- await testSetup.renderOnce();
831
- expect(testSetup.captureCharFrame()).toContain("Pane Settings");
832
-
833
- testSetup.renderer.destroy();
834
- testSetup = await testRender(<CommandBarHarness query="PS" hasPaneSettings={() => false} />, {
835
- width: 80,
836
- height: 24,
837
- });
838
-
839
- await testSetup.renderOnce();
840
- expect(testSetup.captureCharFrame()).not.toContain("Pane Settings");
841
- });
842
-
843
- test("collapses the trailing column on narrow terminals", async () => {
844
- testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
845
- width: 42,
846
- height: 20,
847
- });
848
-
849
- await testSetup.renderOnce();
850
-
851
- expect(testSetup.captureCharFrame()).toMatchSnapshot();
852
- });
853
-
854
- test("moves the theme selector with arrow keys", async () => {
855
- testSetup = await testRender(<CommandBarHarness query="TH " live />, {
856
- width: 80,
857
- height: 24,
858
- });
859
-
860
- await testSetup.renderOnce();
861
- await act(async () => {
862
- testSetup!.mockInput.pressArrow("down");
863
- await Bun.sleep(220);
864
- await testSetup!.renderOnce();
865
- });
866
- await testSetup.renderOnce();
867
-
868
- const frame = testSetup.captureCharFrame();
869
- expect(frame).toContain("theme:green");
870
- });
871
-
872
- test("debounces rapid keyboard theme previews", async () => {
873
- const actions: AppAction[] = [];
874
- testSetup = await testRender(
875
- <CommandBarHarness query="TH " live onAction={(action) => actions.push(action)} />,
876
- { width: 80, height: 24 },
877
- );
878
-
879
- await testSetup.renderOnce();
880
- await act(async () => {
881
- for (let index = 0; index < 6; index++) {
882
- testSetup!.mockInput.pressArrow("down");
883
- }
884
- await testSetup!.renderOnce();
885
- });
886
-
887
- expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(0);
888
-
889
- await act(async () => {
890
- await Bun.sleep(100);
891
- await testSetup!.renderOnce();
892
- });
893
-
894
- expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(0);
895
-
896
- await act(async () => {
897
- await Bun.sleep(150);
898
- await testSetup!.renderOnce();
899
- });
900
-
901
- expect(actions.filter((action) => action.type === "PREVIEW_THEME")).toHaveLength(1);
902
- });
903
-
904
- test("does not preview a theme from mouse hover alone", async () => {
905
- testSetup = await testRender(<CommandBarHarness query="TH " live />, {
906
- width: 80,
907
- height: 24,
908
- });
909
-
910
- await testSetup.renderOnce();
911
- const frame = testSetup.captureCharFrame();
912
- const rows = frame.split("\n");
913
- const hoveredRow = rows.findIndex((line) => line.includes("Green"));
914
- const hoveredCol = rows[hoveredRow]?.indexOf("Green") ?? -1;
915
-
916
- expect(hoveredRow).toBeGreaterThanOrEqual(0);
917
- expect(hoveredCol).toBeGreaterThanOrEqual(0);
918
-
919
- await act(async () => {
920
- await testSetup!.mockMouse.moveTo(hoveredCol + 1, hoveredRow);
921
- await testSetup!.renderOnce();
922
- });
923
- await testSetup.renderOnce();
924
-
925
- const hoveredFrame = testSetup.captureCharFrame();
926
- expect(hoveredFrame).toContain("theme:amber");
927
- expect(hoveredFrame).not.toContain("theme:green");
928
- });
929
-
930
- test("keeps the selected theme after pressing enter in root theme mode", async () => {
931
- testSetup = await testRender(<CommandBarHarness query="TH " live />, {
932
- width: 80,
933
- height: 24,
934
- });
935
-
936
- await testSetup.renderOnce();
937
- await act(async () => {
938
- testSetup!.mockInput.pressArrow("down");
939
- await Bun.sleep(220);
940
- await testSetup!.renderOnce();
941
- });
942
- await testSetup.renderOnce();
943
-
944
- await act(async () => {
945
- testSetup!.mockInput.pressEnter();
946
- await Bun.sleep(0);
947
- await testSetup!.renderOnce();
948
- });
949
- await testSetup.renderOnce();
950
-
951
- const frame = testSetup.captureCharFrame();
952
- expect(frame).toContain("theme:green");
953
- expect(frame).not.toContain("theme:amber");
954
- expect(frame).not.toContain("Commands");
955
- });
956
-
957
- test("uses pending keyboard theme selection when enter follows before a repaint", async () => {
958
- testSetup = await testRender(<CommandBarHarness query="TH " live />, {
959
- width: 80,
960
- height: 24,
961
- });
962
-
963
- await testSetup.renderOnce();
964
- await act(async () => {
965
- testSetup!.mockInput.pressArrow("down");
966
- testSetup!.mockInput.pressEnter();
967
- await Bun.sleep(0);
968
- await testSetup!.renderOnce();
969
- });
970
- await testSetup.renderOnce();
971
-
972
- const frame = testSetup.captureCharFrame();
973
- expect(frame).toContain("theme:green");
974
- expect(frame).not.toContain("theme:amber");
975
- expect(frame).not.toContain("Commands");
976
- });
977
-
978
- test("clears theme preview when leaving root theme mode", async () => {
979
- testSetup = await testRender(<CommandBarHarness query="TH " live />, {
980
- width: 80,
981
- height: 24,
982
- });
983
-
984
- await testSetup.renderOnce();
985
- await act(async () => {
986
- testSetup!.mockInput.pressArrow("down");
987
- await Bun.sleep(220);
988
- await testSetup!.renderOnce();
989
- });
990
- await testSetup.renderOnce();
991
- expect(testSetup.captureCharFrame()).toContain("theme:green");
992
-
993
- await emitKeypress(testSetup, { name: "u", ctrl: true });
994
- await testSetup.renderOnce();
995
-
996
- const frame = testSetup.captureCharFrame();
997
- expect(frame).toContain("theme:amber");
998
- expect(frame).not.toContain("theme:green");
999
- expect(frame).toContain("Commands");
1000
- });
1001
-
1002
- test("keeps typed prefixes in the root query until a result is activated", async () => {
1003
- testSetup = await testRender(<CommandBarHarness query="DES " />, {
1004
- width: 80,
1005
- height: 24,
1006
- });
1007
-
1008
- await testSetup.renderOnce();
1009
-
1010
- let frame = testSetup.captureCharFrame();
1011
- expect(frame).toContain("Commands");
1012
- expect(frame).toContain("DES");
1013
- expect(frame).toContain("Type a ticker symbol");
1014
- expect(frame).not.toContain("Back");
1015
- });
1016
-
1017
- test("QQ without an active ticker opens inline ticker-list entry on enter", async () => {
1018
- testSetup = await testRender(<CommandBarHarness query="QQ" />, {
1019
- width: 100,
1020
- height: 20,
1021
- });
1022
-
1023
- await testSetup.renderOnce();
1024
- expect(testSetup.captureCharFrame()).toContain("Quote Monitor");
1025
- expect(testSetup.captureCharFrame()).not.toContain("Back");
1026
-
1027
- await act(async () => {
1028
- testSetup!.mockInput.pressEnter();
1029
- await testSetup!.renderOnce();
1030
- });
1031
-
1032
- const frame = testSetup.captureCharFrame();
1033
- expect(frame).toContain("Back");
1034
- expect(frame).toContain("Quote Tickers");
1035
- });
1036
-
1037
- test("T without an active ticker opens ticker search on enter", async () => {
1038
- testSetup = await testRender(<CommandBarHarness query="T" />, {
1039
- width: 100,
1040
- height: 20,
1041
- });
1042
-
1043
- await testSetup.renderOnce();
1044
- expect(testSetup.captureCharFrame()).toContain("Description");
1045
-
1046
- await act(async () => {
1047
- testSetup!.mockInput.pressEnter();
1048
- await testSetup!.renderOnce();
1049
- });
1050
-
1051
- const frame = testSetup.captureCharFrame();
1052
- expect(frame).toContain("Back");
1053
- expect(frame).toContain("Security Description");
1054
- });
1055
-
1056
- test("QQ with an active ticker shows ghost completion and tab inserts the symbol", async () => {
1057
- testSetup = await testRender(<CommandBarHarness query="QQ" live selectedTicker="AAPL" showQueryState />, {
1058
- width: 100,
1059
- height: 20,
1060
- });
1061
-
1062
- await testSetup.renderOnce();
1063
- expect(testSetup.captureCharFrame()).toContain("QQ AAPL");
1064
- expect(testSetup.captureCharFrame()).toContain("Shortcut: Quote Monitor for AAPL");
1065
- expect(testSetup.captureCharFrame()).toContain("query:QQ");
1066
-
1067
- await act(async () => {
1068
- testSetup!.mockInput.pressTab();
1069
- await testSetup!.renderOnce();
1070
- });
1071
-
1072
- const frame = testSetup.captureCharFrame();
1073
- expect(frame).toContain("query:QQ AAPL");
1074
- });
1075
-
1076
- test("typing a shorthand and pressing enter executes the inferred quote monitor shortcut", async () => {
1077
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
1078
-
1079
- testSetup = await testRender(<CommandBarHarness
1080
- query=""
1081
- live
1082
- selectedTicker="AAPL"
1083
- configurePluginRegistry={(pluginRegistry) => {
1084
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
1085
- created.push({ templateId, options });
1086
- };
1087
- }}
1088
- />, {
1089
- width: 100,
1090
- height: 20,
1091
- });
1092
-
1093
- await testSetup.renderOnce();
1094
-
1095
- await act(async () => {
1096
- await testSetup!.mockInput.typeText("QQ");
1097
- testSetup!.mockInput.pressEnter();
1098
- await Bun.sleep(0);
1099
- await testSetup!.renderOnce();
1100
- });
1101
-
1102
- expect(created).toEqual([{
1103
- templateId: "quote-monitor-pane",
1104
- options: {
1105
- arg: "AAPL",
1106
- symbols: ["AAPL"],
1107
- },
1108
- }]);
1109
- });
1110
-
1111
- test("consumes enter before focused pane shortcuts when executing a pane shortcut", async () => {
1112
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
1113
- let leakedEnterCount = 0;
1114
-
1115
- testSetup = await testRender(<CommandBarHarness
1116
- query="PF"
1117
- live
1118
- onUnhandledEnter={() => {
1119
- leakedEnterCount += 1;
1120
- }}
1121
- configurePluginRegistry={(pluginRegistry) => {
1122
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
1123
- created.push({ templateId, options });
1124
- };
1125
- }}
1126
- />, {
1127
- width: 100,
1128
- height: 20,
1129
- });
1130
-
1131
- await testSetup.renderOnce();
1132
-
1133
- await act(async () => {
1134
- testSetup!.mockInput.pressEnter();
1135
- await Bun.sleep(0);
1136
- await testSetup!.renderOnce();
1137
- });
1138
-
1139
- expect(created).toEqual([{ templateId: "new-portfolio-pane", options: undefined }]);
1140
- expect(leakedEnterCount).toBe(0);
1141
- });
1142
-
1143
- test("typing a chat channel shortcut opens that channel directly", async () => {
1144
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
1145
-
1146
- testSetup = await testRender(<CommandBarHarness
1147
- query=""
1148
- live
1149
- configurePluginRegistry={(pluginRegistry) => {
1150
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
1151
- created.push({ templateId, options });
1152
- };
1153
- }}
1154
- />, {
1155
- width: 100,
1156
- height: 20,
1157
- });
1158
-
1159
- await testSetup.renderOnce();
1160
-
1161
- await act(async () => {
1162
- await testSetup!.mockInput.typeText("CHAT help");
1163
- testSetup!.mockInput.pressEnter();
1164
- await Bun.sleep(0);
1165
- await testSetup!.renderOnce();
1166
- });
1167
-
1168
- expect(created).toEqual([{
1169
- templateId: "new-chat-pane",
1170
- options: {
1171
- arg: "help",
1172
- },
1173
- }]);
1174
- });
1175
-
1176
- test("clears the root query with cmd-backspace", async () => {
1177
- testSetup = await testRender(<CommandBarHarness query="DES AMD" />, {
1178
- width: 80,
1179
- height: 24,
1180
- });
1181
-
1182
- await testSetup.renderOnce();
1183
- expect(testSetup.captureCharFrame()).toContain("DES AMD");
1184
-
1185
- await act(async () => {
1186
- testSetup!.mockInput.pressKey("backspace", { meta: true });
1187
- await testSetup!.renderOnce();
1188
- });
1189
-
1190
- const frame = testSetup.captureCharFrame();
1191
- expect(frame).toContain("Commands");
1192
- expect(frame).toContain("Search");
1193
- expect(frame).not.toContain("DES AMD");
1194
- });
1195
-
1196
- test("pressing the close shortcut at the root closes the command bar", async () => {
1197
- testSetup = await testRender(<CommandBarHarness query="" live />, {
1198
- width: 80,
1199
- height: 24,
1200
- });
1201
-
1202
- await testSetup.renderOnce();
1203
-
1204
- await act(async () => {
1205
- testSetup!.mockInput.pressKey("`");
1206
- await testSetup!.renderOnce();
1207
- });
1208
-
1209
- expect(testSetup.captureCharFrame()).not.toContain("Commands");
1210
- });
1211
-
1212
- test("loads provider-backed ticker search results in the root command results", async () => {
1213
- testSetup = await testRender(
1214
- <CommandBarHarness
1215
- query="DES appl"
1216
- dataProvider={makeDataProvider(async () => [
1217
- { providerId: "yahoo", symbol: "IVSX", name: "Invsivx Holdings", exchange: "NYSE", type: "ETF" },
1218
- { providerId: "yahoo", symbol: "AAPL", name: "Apple Inc", exchange: "NASDAQ", type: "EQUITY" },
1219
- { providerId: "yahoo", symbol: "AMAT", name: "Applied Materials", exchange: "NASDAQ", type: "EQUITY" },
1220
- { providerId: "yahoo", symbol: "AAOI", name: "Applied Optoelectronics", exchange: "NASDAQ", type: "EQUITY" },
1221
- { providerId: "yahoo", symbol: "APP", name: "AppLovin Corp", exchange: "NASDAQ", type: "EQUITY" },
1222
- ])}
1223
- />,
1224
- { width: 80, height: 24 },
1225
- );
1226
-
1227
- await testSetup.renderOnce();
1228
- const frame = await waitForFrameToContain("AAOI");
1229
- expect(frame).toContain("Saved");
1230
- expect(frame).toContain("Other Listings");
1231
- expect(frame).toContain("APP");
1232
- expect(frame).toContain("AAOI");
1233
- });
1234
-
1235
- test("keeps exact T ticker matches above noisy provider matches", async () => {
1236
- testSetup = await testRender(
1237
- <CommandBarHarness
1238
- query="T AMD"
1239
- extraTickers={[
1240
- makeTicker("AMDL", "GraniteShares 2x Long AMD Daily"),
1241
- makeTicker("AMUU", "Direxion Daily AMD Bull 2X ETF"),
1242
- ]}
1243
- dataProvider={makeDataProvider(async () => [
1244
- { providerId: "yahoo", symbol: "DES", name: "AMD-themed synthetic listing", exchange: "ASX", type: "EQUITY" },
1245
- { providerId: "yahoo", symbol: "DES2", name: "AMD-themed secondary listing", exchange: "LSE", type: "EQUITY" },
1246
- { providerId: "yahoo", symbol: "AMD", name: "Advanced Micro Devices", exchange: "NASDAQ", type: "EQUITY" },
1247
- ])}
1248
- />,
1249
- { width: 100, height: 24 },
1250
- );
1251
-
1252
- await testSetup.renderOnce();
1253
- const frame = await waitForFrameToContain("DES2");
1254
- const resultLines = frame
1255
- .split("\n")
1256
- .filter((line) => /\b(AMD|DES2?|DES)\b/.test(line) && /\b(NASDAQ|ASX|LSE)\b/.test(line));
1257
-
1258
- expect(resultLines[0]).toContain("AMD");
1259
- expect(resultLines[0]).toContain("NASDAQ");
1260
- });
1261
-
1262
- test("loads exact provider matches for plain ticker-shaped root queries", async () => {
1263
- testSetup = await testRender(
1264
- <CommandBarHarness
1265
- query="amd"
1266
- extraTickers={[
1267
- makeTicker("AMDL", "GraniteShares 2x Long AMD Daily"),
1268
- makeTicker("AMUU", "Direxion Daily AMD Bull 2X ETF"),
1269
- ]}
1270
- dataProvider={makeDataProvider(async () => [
1271
- { providerId: "yahoo", symbol: "DES", name: "AMD-themed synthetic listing", exchange: "ASX", type: "EQUITY" },
1272
- { providerId: "yahoo", symbol: "AMD", name: "Advanced Micro Devices", exchange: "NASDAQ", type: "EQUITY" },
1273
- ])}
1274
- />,
1275
- { width: 100, height: 24 },
1276
- );
1277
-
1278
- await testSetup.renderOnce();
1279
- const frame = await waitForFrameToContain("Exact Match");
1280
- const resultLines = frame
1281
- .split("\n")
1282
- .filter((line) => /\b(AMD|AMDL|AMUU|DES)\b/.test(line) && /\b(NASDAQ|ASX)\b/.test(line));
1283
-
1284
- expect(resultLines[0]).toContain("AMD");
1285
- expect(resultLines[0]).toContain("NASDAQ");
1286
- });
1287
-
1288
- test("keeps pane matches visible alongside plain ticker provider results", async () => {
1289
- testSetup = await testRender(
1290
- <CommandBarHarness
1291
- query="opti"
1292
- dataProvider={makeDataProvider(async () => [
1293
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "CSE", type: "EQUITY" },
1294
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "NEO", type: "EQUITY" },
1295
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "LSE", type: "EQUITY" },
1296
- ])}
1297
- configurePluginRegistry={(pluginRegistry) => {
1298
- const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
1299
- paneTemplates.set("options-pane", {
1300
- id: "options-pane",
1301
- paneId: "options",
1302
- label: "Options",
1303
- description: "Options chain for the selected ticker",
1304
- shortcut: { prefix: "OMON", argPlaceholder: "ticker", argKind: "ticker" },
1305
- canCreate: (context: any, options?: PaneTemplateCreateOptions) => (
1306
- context.activeTicker !== null || !!options?.arg
1307
- ),
1308
- });
1309
- }}
1310
- />,
1311
- { width: 100, height: 24 },
1312
- );
1313
-
1314
- await testSetup.renderOnce();
1315
- const frame = await waitForFrameToContain("OPTI");
1316
- expect(frame).toContain("Options");
1317
- expect(frame).toContain("OMON");
1318
- });
1319
-
1320
- test("clears stale provider rows after a plain ticker root search is cleared", async () => {
1321
- testSetup = await testRender(
1322
- <CommandBarHarness
1323
- query=""
1324
- live
1325
- dataProvider={makeDataProvider(async () => [
1326
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "CSE", type: "EQUITY" },
1327
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "NEO", type: "EQUITY" },
1328
- { providerId: "yahoo", symbol: "OPTI", name: "Optimi Health Corp", exchange: "LSE", type: "EQUITY" },
1329
- ])}
1330
- />,
1331
- { width: 100, height: 24 },
1332
- );
1333
-
1334
- await testSetup.renderOnce();
1335
- await act(async () => {
1336
- await testSetup!.mockInput.typeText("opti");
1337
- await testSetup!.renderOnce();
1338
- });
1339
-
1340
- await waitForFrameToContain("OPTI");
1341
- await emitKeypress(testSetup, { name: "u", ctrl: true });
1342
- await renderFrames(2);
1343
-
1344
- const frame = testSetup.captureCharFrame();
1345
- expect(frame).toContain("Search");
1346
- expect(frame).toContain("Tickers");
1347
- expect(frame).not.toContain("OPTI");
1348
- });
1349
-
1350
- test("DES MSFT opens an exact ticker directly", async () => {
1351
- const pinned: string[] = [];
1352
-
1353
- testSetup = await testRender(
1354
- <CommandBarHarness
1355
- query="DES MSFT"
1356
- configurePluginRegistry={(pluginRegistry) => {
1357
- pluginRegistry.pinTicker = (symbol) => {
1358
- pinned.push(symbol);
1359
- };
1360
- }}
1361
- />,
1362
- { width: 100, height: 20 },
1363
- );
1364
-
1365
- await testSetup.renderOnce();
1366
- await act(async () => {
1367
- testSetup!.mockInput.pressEnter();
1368
- await Bun.sleep(0);
1369
- await testSetup!.renderOnce();
1370
- await testSetup!.renderOnce();
1371
- });
1372
-
1373
- expect(pinned).toEqual(["MSFT"]);
1374
- });
1375
-
1376
- test("T AMD opens an exact ticker directly", async () => {
1377
- const pinned: string[] = [];
1378
-
1379
- testSetup = await testRender(
1380
- <CommandBarHarness
1381
- query="T AMD"
1382
- extraTickers={[makeTicker("AMD", "Advanced Micro Devices")]}
1383
- configurePluginRegistry={(pluginRegistry) => {
1384
- pluginRegistry.pinTicker = (symbol) => {
1385
- pinned.push(symbol);
1386
- };
1387
- }}
1388
- />,
1389
- { width: 100, height: 20 },
1390
- );
1391
-
1392
- await testSetup.renderOnce();
1393
- await act(async () => {
1394
- testSetup!.mockInput.pressEnter();
1395
- await Bun.sleep(0);
1396
- await testSetup!.renderOnce();
1397
- await testSetup!.renderOnce();
1398
- });
1399
-
1400
- expect(pinned).toEqual(["AMD"]);
1401
- });
1402
-
1403
- test("AW AAPL uses the active watchlist target by default", async () => {
1404
- const saved: TickerRecord[] = [];
1405
-
1406
- testSetup = await testRender(
1407
- <CommandBarHarness
1408
- query="AW AAPL"
1409
- onSaveTicker={(ticker) => {
1410
- saved.push(ticker);
1411
- }}
1412
- configureState={(state) => ({
1413
- ...state,
1414
- paneState: {
1415
- ...state.paneState,
1416
- "portfolio-list:main": {
1417
- collectionId: "watchlist",
1418
- },
1419
- },
1420
- })}
1421
- />,
1422
- { width: 100, height: 20 },
1423
- );
1424
-
1425
- await testSetup.renderOnce();
1426
- await act(async () => {
1427
- testSetup!.mockInput.pressEnter();
1428
- await Bun.sleep(0);
1429
- await testSetup!.renderOnce();
1430
- });
1431
-
1432
- expect(saved.at(-1)?.metadata.watchlists).toEqual(["watchlist"]);
1433
- });
1434
-
1435
- test("bare AW without a compatible active target adds to the sole watchlist directly", async () => {
1436
- const saved: TickerRecord[] = [];
1437
-
1438
- testSetup = await testRender(
1439
- <CommandBarHarness
1440
- query="AW"
1441
- selectedTicker="AAPL"
1442
- onSaveTicker={(ticker) => {
1443
- saved.push(ticker);
1444
- }}
1445
- />,
1446
- { width: 100, height: 20 },
1447
- );
1448
-
1449
- await testSetup.renderOnce();
1450
- await act(async () => {
1451
- testSetup!.mockInput.pressEnter();
1452
- await Bun.sleep(0);
1453
- await testSetup!.renderOnce();
1454
- });
1455
-
1456
- expect(saved.at(-1)?.metadata.watchlists).toEqual(["watchlist"]);
1457
- });
1458
-
1459
- test("bare AW without a compatible active target opens inline target selection when multiple watchlists exist", async () => {
1460
- testSetup = await testRender(
1461
- <CommandBarHarness
1462
- query="AW"
1463
- selectedTicker="AAPL"
1464
- configureConfig={(config) => ({
1465
- ...config,
1466
- watchlists: [
1467
- { id: "watchlist", name: "Watchlist" },
1468
- { id: "growth", name: "Growth Radar" },
1469
- ],
1470
- })}
1471
- />,
1472
- { width: 100, height: 20 },
1473
- );
1474
-
1475
- await testSetup.renderOnce();
1476
- await act(async () => {
1477
- testSetup!.mockInput.pressEnter();
1478
- await testSetup!.renderOnce();
1479
- });
1480
-
1481
- const frame = testSetup.captureCharFrame();
1482
- expect(frame).toContain("Add AAPL to Watchlist");
1483
- expect(frame).toContain("Watchlist");
1484
- expect(frame).toContain("Back");
1485
- });
1486
-
1487
- test("typing add still surfaces Add to Portfolio for a ticker already in the active manual portfolio", async () => {
1488
- testSetup = await testRender(
1489
- <CommandBarHarness
1490
- query="add"
1491
- selectedTicker="AAPL"
1492
- configureConfig={(config) => ({
1493
- ...config,
1494
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1495
- })}
1496
- configureState={(state) => ({
1497
- ...state,
1498
- paneState: {
1499
- ...state.paneState,
1500
- "portfolio-list:main": {
1501
- collectionId: "research",
1502
- cursorSymbol: "AAPL",
1503
- },
1504
- },
1505
- })}
1506
- extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1507
- portfolios: ["research"],
1508
- })]}
1509
- />,
1510
- { width: 100, height: 20 },
1511
- );
1512
-
1513
- await testSetup.renderOnce();
1514
-
1515
- const frame = testSetup.captureCharFrame();
1516
- expect(frame).toContain("Add AAPL to Portfolio");
1517
- });
1518
-
1519
- test("AP opens the add-to-portfolio workflow and prefills avg cost from the current price", async () => {
1520
- testSetup = await testRender(
1521
- <CommandBarHarness
1522
- query="AP AAPL"
1523
- selectedTicker="AAPL"
1524
- configureConfig={(config) => ({
1525
- ...config,
1526
- portfolios: [{ id: "research", name: "Only Manual Portfolio", currency: "USD" }],
1527
- })}
1528
- configureState={(state) => ({
1529
- ...state,
1530
- paneState: {
1531
- ...state.paneState,
1532
- "portfolio-list:main": {
1533
- collectionId: "research",
1534
- cursorSymbol: "AAPL",
1535
- },
1536
- },
1537
- financials: new Map([["AAPL", {
1538
- annualStatements: [],
1539
- quarterlyStatements: [],
1540
- priceHistory: [],
1541
- quote: {
1542
- symbol: "AAPL",
1543
- price: 205.5,
1544
- currency: "USD",
1545
- change: 1.25,
1546
- changePercent: 0.61,
1547
- lastUpdated: Date.now(),
1548
- },
1549
- }]]),
1550
- })}
1551
- extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1552
- portfolios: ["research"],
1553
- })]}
1554
- />,
1555
- { width: 100, height: 30 },
1556
- );
1557
-
1558
- await testSetup.renderOnce();
1559
- await act(async () => {
1560
- testSetup!.mockInput.pressEnter();
1561
- await testSetup!.renderOnce();
1562
- });
1563
-
1564
- const frame = await waitForFrameToContain("Avg Cost");
1565
- expect(frame).toContain("Shares");
1566
- expect(frame).toContain("Avg Cost");
1567
- expect(frame).toContain("205.5");
1568
- expect(frame).not.toContain("Only Manual Portfolio");
1569
- expectSingleBackControl(frame);
1570
- });
1571
-
1572
- test("add-to-portfolio can still add membership without entering a position", async () => {
1573
- const saved: TickerRecord[] = [];
1574
-
1575
- testSetup = await testRender(
1576
- <CommandBarHarness
1577
- query="AP AAPL"
1578
- selectedTicker="AAPL"
1579
- onSaveTicker={(ticker) => {
1580
- saved.push(ticker);
1581
- }}
1582
- configureConfig={(config) => ({
1583
- ...config,
1584
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1585
- })}
1586
- configureState={(state) => ({
1587
- ...state,
1588
- paneState: {
1589
- ...state.paneState,
1590
- "portfolio-list:main": {
1591
- collectionId: "research",
1592
- cursorSymbol: "AAPL",
1593
- },
1594
- },
1595
- })}
1596
- />,
1597
- { width: 100, height: 30 },
1598
- );
1599
-
1600
- await testSetup.renderOnce();
1601
- await act(async () => {
1602
- testSetup!.mockInput.pressEnter();
1603
- await testSetup!.renderOnce();
1604
- });
1605
-
1606
- const frame = await waitForFrameToContain("Avg Cost");
1607
- expect(frame).toContain("Shares");
1608
- expect(frame).toContain("Avg Cost");
1609
-
1610
- await clickFrameText("Add to Portfolio");
1611
- await act(async () => {
1612
- await Bun.sleep(0);
1613
- await testSetup!.renderOnce();
1614
- });
1615
-
1616
- expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
1617
- expect(saved.at(-1)?.metadata.positions).toEqual([]);
1618
- });
1619
-
1620
- test("only surfaces Set Portfolio Position when a manual portfolio exists", async () => {
1621
- testSetup = await testRender(
1622
- <CommandBarHarness query="Set Portfolio Position" />,
1623
- { width: 100, height: 20 },
1624
- );
1625
-
1626
- await testSetup.renderOnce();
1627
- expect(testSetup.captureCharFrame()).toContain("Set Portfolio Position");
1628
-
1629
- testSetup.renderer.destroy();
1630
- testSetup = await testRender(
1631
- <CommandBarHarness
1632
- query="Set Portfolio Position"
1633
- configureConfig={(config) => ({
1634
- ...config,
1635
- portfolios: [{
1636
- id: "broker:ibkr",
1637
- name: "IBKR Account",
1638
- currency: "USD",
1639
- brokerId: "ibkr",
1640
- brokerInstanceId: "ibkr-live",
1641
- }],
1642
- })}
1643
- />,
1644
- { width: 100, height: 20 },
1645
- );
1646
-
1647
- await testSetup.renderOnce();
1648
- const frame = testSetup.captureCharFrame();
1649
- expect(frame).toContain('No matches for "Set Portfolio Position"');
1650
- expect(frame).not.toContain("Create or update a manual position in a portfolio");
1651
- });
1652
-
1653
- test("matches set portfolio position when searching edit position", async () => {
1654
- testSetup = await testRender(
1655
- <CommandBarHarness
1656
- query="edit position"
1657
- selectedTicker="AAPL"
1658
- configureConfig={(config) => ({
1659
- ...config,
1660
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1661
- })}
1662
- configureState={(state) => ({
1663
- ...state,
1664
- paneState: {
1665
- ...state.paneState,
1666
- "portfolio-list:main": {
1667
- collectionId: "research",
1668
- cursorSymbol: "AAPL",
1669
- },
1670
- },
1671
- })}
1672
- />,
1673
- { width: 100, height: 20 },
1674
- );
1675
-
1676
- await testSetup.renderOnce();
1677
-
1678
- const frame = testSetup.captureCharFrame();
1679
- expect(frame).toContain("Set Position for AAPL");
1680
- });
1681
-
1682
- test("prefills the portfolio position workflow from the active manual portfolio and ticker", async () => {
1683
- testSetup = await testRender(
1684
- <CommandBarHarness
1685
- query="Set Position for AAPL"
1686
- selectedTicker="AAPL"
1687
- configureConfig={(config) => ({
1688
- ...config,
1689
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1690
- })}
1691
- configureState={(state) => ({
1692
- ...state,
1693
- paneState: {
1694
- ...state.paneState,
1695
- "portfolio-list:main": {
1696
- collectionId: "research",
1697
- cursorSymbol: "AAPL",
1698
- },
1699
- },
1700
- })}
1701
- extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1702
- portfolios: ["research"],
1703
- positions: [{
1704
- portfolio: "research",
1705
- shares: 10,
1706
- avgCost: 180,
1707
- currency: "USD",
1708
- broker: "manual",
1709
- }],
1710
- })]}
1711
- />,
1712
- { width: 100, height: 30 },
1713
- );
1714
-
1715
- await testSetup.renderOnce();
1716
- await act(async () => {
1717
- testSetup!.mockInput.pressEnter();
1718
- await testSetup!.renderOnce();
1719
- });
1720
-
1721
- const frame = await waitForFrameToContain("Avg Cost");
1722
- expect(frame).toContain("Research");
1723
- expect(frame).toContain("AAPL");
1724
- expect(frame).toContain("10");
1725
- expect(frame).toContain("180");
1726
- expectSingleBackControl(frame);
1727
- });
1728
-
1729
- test("submits the portfolio position workflow and persists a manual position", async () => {
1730
- const saved: TickerRecord[] = [];
1731
-
1732
- testSetup = await testRender(
1733
- <CommandBarHarness
1734
- query="Set Position for AAPL"
1735
- selectedTicker="AAPL"
1736
- onSaveTicker={(ticker) => {
1737
- saved.push(ticker);
1738
- }}
1739
- configureConfig={(config) => ({
1740
- ...config,
1741
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1742
- })}
1743
- configureState={(state) => ({
1744
- ...state,
1745
- paneState: {
1746
- ...state.paneState,
1747
- "portfolio-list:main": {
1748
- collectionId: "research",
1749
- cursorSymbol: "AAPL",
1750
- },
1751
- },
1752
- })}
1753
- />,
1754
- { width: 100, height: 30 },
1755
- );
1756
-
1757
- await testSetup.renderOnce();
1758
- await act(async () => {
1759
- testSetup!.mockInput.pressEnter();
1760
- await testSetup!.renderOnce();
1761
- });
1762
-
1763
- await act(async () => {
1764
- testSetup!.mockInput.pressTab();
1765
- await testSetup!.renderOnce();
1766
- });
1767
- await act(async () => {
1768
- testSetup!.mockInput.pressTab();
1769
- await testSetup!.renderOnce();
1770
- });
1771
- await act(async () => {
1772
- await testSetup!.mockInput.typeText("10");
1773
- await testSetup!.renderOnce();
1774
- });
1775
- await act(async () => {
1776
- testSetup!.mockInput.pressTab();
1777
- await testSetup!.renderOnce();
1778
- });
1779
- await act(async () => {
1780
- await testSetup!.mockInput.typeText("180");
1781
- await testSetup!.renderOnce();
1782
- });
1783
- await act(async () => {
1784
- testSetup!.mockInput.pressTab();
1785
- await testSetup!.renderOnce();
1786
- });
1787
- await act(async () => {
1788
- await testSetup!.mockInput.typeText("EUR");
1789
- await testSetup!.renderOnce();
1790
- });
1791
- await act(async () => {
1792
- testSetup!.mockInput.pressEnter();
1793
- await Bun.sleep(0);
1794
- await testSetup!.renderOnce();
1795
- });
1796
-
1797
- expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
1798
- expect(saved.at(-1)?.metadata.positions).toEqual([{
1799
- portfolio: "research",
1800
- shares: 10,
1801
- avgCost: 180,
1802
- currency: "EUR",
1803
- broker: "manual",
1804
- }]);
1805
- });
1806
-
1807
- test("removing a ticker from a manual portfolio also removes its position", async () => {
1808
- const saved: TickerRecord[] = [];
1809
-
1810
- testSetup = await testRender(
1811
- <CommandBarHarness
1812
- query="RP AAPL"
1813
- selectedTicker="AAPL"
1814
- onSaveTicker={(ticker) => {
1815
- saved.push(ticker);
1816
- }}
1817
- configureConfig={(config) => ({
1818
- ...config,
1819
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1820
- })}
1821
- configureState={(state) => ({
1822
- ...state,
1823
- paneState: {
1824
- ...state.paneState,
1825
- "portfolio-list:main": {
1826
- collectionId: "research",
1827
- cursorSymbol: "AAPL",
1828
- },
1829
- },
1830
- })}
1831
- extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1832
- portfolios: ["research"],
1833
- positions: [{
1834
- portfolio: "research",
1835
- shares: 4,
1836
- avgCost: 175,
1837
- currency: "USD",
1838
- broker: "manual",
1839
- }],
1840
- })]}
1841
- />,
1842
- { width: 100, height: 20 },
1843
- );
1844
-
1845
- await testSetup.renderOnce();
1846
- await act(async () => {
1847
- testSetup!.mockInput.pressEnter();
1848
- await Bun.sleep(0);
1849
- await testSetup!.renderOnce();
1850
- });
1851
-
1852
- expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
1853
- expect(saved.at(-1)?.metadata.positions).toEqual([]);
1854
- });
1855
-
1856
- test("deleting a manual portfolio also cleans saved ticker positions", async () => {
1857
- const saved: TickerRecord[] = [];
1858
-
1859
- testSetup = await testRender(
1860
- <CommandBarHarness
1861
- query="Delete Portfolio"
1862
- onSaveTicker={(ticker) => {
1863
- saved.push(ticker);
1864
- }}
1865
- configureConfig={(config) => ({
1866
- ...config,
1867
- portfolios: [{ id: "research", name: "Research", currency: "USD" }],
1868
- })}
1869
- configureState={(state) => ({
1870
- ...state,
1871
- paneState: {
1872
- ...state.paneState,
1873
- "portfolio-list:main": {
1874
- collectionId: "research",
1875
- cursorSymbol: "AAPL",
1876
- },
1877
- },
1878
- })}
1879
- extraTickers={[makeTicker("AAPL", "Apple Inc.", {
1880
- portfolios: ["research"],
1881
- positions: [{
1882
- portfolio: "research",
1883
- shares: 2,
1884
- avgCost: 160,
1885
- currency: "USD",
1886
- broker: "manual",
1887
- }],
1888
- })]}
1889
- />,
1890
- { width: 100, height: 20 },
1891
- );
1892
-
1893
- await testSetup.renderOnce();
1894
- await act(async () => {
1895
- testSetup!.mockInput.pressEnter();
1896
- await testSetup!.renderOnce();
1897
- });
1898
- await act(async () => {
1899
- testSetup!.mockInput.pressEnter();
1900
- await testSetup!.renderOnce();
1901
- });
1902
- await act(async () => {
1903
- testSetup!.mockInput.pressEnter();
1904
- await Bun.sleep(0);
1905
- await testSetup!.renderOnce();
1906
- });
1907
-
1908
- expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
1909
- expect(saved.at(-1)?.metadata.positions).toEqual([]);
1910
- });
1911
-
1912
- test("pressing enter after arrow navigation ignores stale mouse hover", async () => {
1913
- const pinned: string[] = [];
1914
-
1915
- testSetup = await testRender(
1916
- <CommandBarHarness
1917
- query=""
1918
- configurePluginRegistry={(pluginRegistry) => {
1919
- pluginRegistry.pinTicker = (symbol) => {
1920
- pinned.push(symbol);
1921
- };
1922
- }}
1923
- />,
1924
- { width: 80, height: 24 },
1925
- );
1926
-
1927
- await testSetup.renderOnce();
1928
-
1929
- const frame = testSetup.captureCharFrame();
1930
- const rows = frame.split("\n");
1931
- const hoveredRow = rows.findIndex((line) => line.includes("AAPL"));
1932
- const hoveredCol = rows[hoveredRow]?.indexOf("AAPL") ?? -1;
1933
-
1934
- expect(hoveredRow).toBeGreaterThanOrEqual(0);
1935
- expect(hoveredCol).toBeGreaterThanOrEqual(0);
1936
-
1937
- await act(async () => {
1938
- await testSetup!.mockMouse.moveTo(hoveredCol + 1, hoveredRow);
1939
- await testSetup!.renderOnce();
1940
- });
1941
-
1942
- await act(async () => {
1943
- testSetup!.mockInput.pressArrow("down");
1944
- await testSetup!.renderOnce();
1945
- });
1946
-
1947
- await act(async () => {
1948
- testSetup!.mockInput.pressEnter();
1949
- await Bun.sleep(0);
1950
- await testSetup!.renderOnce();
1951
- });
1952
-
1953
- expect(pinned).toEqual(["MSFT"]);
1954
- });
1955
-
1956
- test("pressing enter follows the displayed grouped order and opens confirm for the highlighted destructive item", async () => {
1957
- const executed: string[] = [];
1958
-
1959
- testSetup = await testRender(
1960
- <CommandBarHarness
1961
- query="delete"
1962
- configurePluginRegistry={(pluginRegistry) => {
1963
- (pluginRegistry.commands as Map<string, any>).set("delete-layout", {
1964
- id: "delete-layout",
1965
- label: "Delete Layout",
1966
- description: "Delete the current layout preset",
1967
- category: "Layout Manager",
1968
- execute: async () => {
1969
- executed.push("delete-layout");
1970
- },
1971
- });
1972
- }}
1973
- />,
1974
- { width: 100, height: 20 },
1975
- );
1976
-
1977
- await testSetup.renderOnce();
1978
- const initialFrame = testSetup.captureCharFrame();
1979
- expect(initialFrame).toContain("Delete Layout");
1980
-
1981
- await act(async () => {
1982
- testSetup!.mockInput.pressEnter();
1983
- await testSetup!.renderOnce();
1984
- });
1985
-
1986
- let frame = testSetup.captureCharFrame();
1987
- expect(frame).toContain("Delete Layout");
1988
- expect(frame).toContain("Back");
1989
- expectSingleBackControl(frame);
1990
- expect(executed).toEqual([]);
1991
-
1992
- await act(async () => {
1993
- testSetup!.mockInput.pressEnter();
1994
- await Bun.sleep(0);
1995
- await testSetup!.renderOnce();
1996
- });
1997
-
1998
- expect(executed).toEqual(["delete-layout"]);
1999
- });
2000
-
2001
- test("uses backspace to leave confirm routes", async () => {
2002
- testSetup = await testRender(
2003
- <CommandBarHarness
2004
- query="delete"
2005
- configurePluginRegistry={(pluginRegistry) => {
2006
- (pluginRegistry.commands as Map<string, any>).set("delete-layout", {
2007
- id: "delete-layout",
2008
- label: "Delete Layout",
2009
- description: "Delete the current layout preset",
2010
- category: "Layout Manager",
2011
- execute: async () => {},
2012
- });
2013
- }}
2014
- />,
2015
- { width: 100, height: 20 },
2016
- );
2017
-
2018
- await testSetup.renderOnce();
2019
- await act(async () => {
2020
- testSetup!.mockInput.pressEnter();
2021
- await testSetup!.renderOnce();
2022
- });
2023
- await renderFrames();
2024
-
2025
- await act(async () => {
2026
- testSetup!.mockInput.pressBackspace();
2027
- await testSetup!.renderOnce();
2028
- });
2029
- await renderFrames();
2030
-
2031
- const frame = testSetup.captureCharFrame();
2032
- expect(frame).toContain("Delete Layout");
2033
- expect(frame).not.toContain("Back Delete Layout");
2034
- });
2035
-
2036
- test("moves through long result lists with the mouse wheel", async () => {
2037
- testSetup = await testRender(
2038
- <CommandBarHarness
2039
- query="scratch"
2040
- configurePluginRegistry={(pluginRegistry) => {
2041
- const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
2042
- for (let index = 0; index < 20; index++) {
2043
- const suffix = String(index).padStart(2, "0");
2044
- paneTemplates.set(`scratch-${suffix}`, {
2045
- id: `scratch-${suffix}`,
2046
- paneId: "chat",
2047
- label: `Scratch Pane ${suffix}`,
2048
- description: `Open scratch pane ${suffix}`,
2049
- });
2050
- }
2051
- }}
2052
- />,
2053
- { width: 100, height: 18 },
2054
- );
2055
-
2056
- await testSetup.renderOnce();
2057
-
2058
- const initialFrame = testSetup.captureCharFrame();
2059
- expect(initialFrame).toContain("Scratch Pane 00");
2060
- expect(initialFrame).not.toContain("Scratch Pane 12");
2061
-
2062
- const rows = initialFrame.split("\n");
2063
- const scrollRow = rows.findIndex((line) => line.includes("Scratch Pane 00"));
2064
- const scrollCol = rows[scrollRow]?.indexOf("Scratch Pane 00") ?? -1;
2065
-
2066
- expect(scrollRow).toBeGreaterThanOrEqual(0);
2067
- expect(scrollCol).toBeGreaterThanOrEqual(0);
2068
-
2069
- await act(async () => {
2070
- for (let index = 0; index < 12; index++) {
2071
- await testSetup!.mockMouse.scroll(scrollCol + 1, scrollRow, "down");
2072
- await testSetup!.renderOnce();
2073
- }
2074
- });
2075
-
2076
- const scrolledFrame = testSetup.captureCharFrame();
2077
- expect(scrolledFrame).not.toContain("Scratch Pane 00");
2078
- expect(scrolledFrame).toContain("Scratch Pane 12");
2079
- });
2080
-
2081
- test("closes when clicking outside the command bar", async () => {
2082
- testSetup = await testRender(<CommandBarHarness query="" live />, {
2083
- width: 80,
2084
- height: 24,
2085
- });
2086
-
2087
- await testSetup.renderOnce();
2088
-
2089
- await act(async () => {
2090
- await testSetup!.mockMouse.click(0, 0);
2091
- await testSetup!.renderOnce();
2092
- });
2093
- await testSetup.renderOnce();
2094
-
2095
- expect(testSetup.captureCharFrame()).not.toContain("Commands");
2096
- });
2097
-
2098
- const layoutModeConfig = (config: AppConfig): AppConfig => {
2099
- const research = cloneLayout(config.layout);
2100
- research.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
2101
- research.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 36, height: 12 }];
2102
- return {
2103
- ...config,
2104
- layouts: [
2105
- { name: "Default", layout: cloneLayout(config.layout) },
2106
- { name: "Research", layout: research },
2107
- ],
2108
- };
2109
- };
2110
-
2111
- const layoutModeState = (state: AppState): AppState => ({
2112
- ...state,
2113
- layoutHistory: {
2114
- 0: {
2115
- past: [cloneLayout(state.config.layout)],
2116
- future: [],
2117
- },
2118
- },
2119
- });
2120
-
2121
- test("renders layout mode with focused pane actions", async () => {
2122
- testSetup = await testRender(<CommandBarHarness
2123
- query="LAY "
2124
- configureConfig={layoutModeConfig}
2125
- configureState={layoutModeState}
2126
- />, {
2127
- width: 90,
2128
- height: 28,
2129
- });
2130
-
2131
- await testSetup.renderOnce();
2132
-
2133
- const frame = testSetup.captureCharFrame();
2134
- expect(frame).toContain("Focused Pane");
2135
- expect(frame).toContain("Float Pane");
2136
- expect(frame).toContain("Undo Layout Change");
2137
- expect(frame).toContain("Current Layout");
2138
- });
2139
-
2140
- test("runs layout actions directly from root search", async () => {
2141
- const actions: AppAction[] = [];
2142
-
2143
- testSetup = await testRender(<CommandBarHarness
2144
- query="undo layout change"
2145
- live
2146
- configureConfig={layoutModeConfig}
2147
- configureState={layoutModeState}
2148
- onAction={(action) => actions.push(action)}
2149
- />, {
2150
- width: 90,
2151
- height: 24,
2152
- });
2153
-
2154
- await testSetup.renderOnce();
2155
-
2156
- expect(testSetup.captureCharFrame()).toContain("Undo Layout Change");
2157
-
2158
- await act(async () => {
2159
- testSetup!.mockInput.pressEnter();
2160
- await testSetup!.renderOnce();
2161
- });
2162
-
2163
- expect(actions.some((action) => action.type === "UNDO_LAYOUT")).toBe(true);
2164
- });
2165
-
2166
- test("renders filtered saved layouts with textual previews", async () => {
2167
- testSetup = await testRender(<CommandBarHarness
2168
- query="LAY Research"
2169
- configureConfig={layoutModeConfig}
2170
- configureState={layoutModeState}
2171
- />, {
2172
- width: 120,
2173
- height: 18,
2174
- });
2175
-
2176
- await testSetup.renderOnce();
2177
-
2178
- const frame = testSetup.captureCharFrame();
2179
- expect(frame).toContain("Research");
2180
- expect(frame).toContain("1c / 1d");
2181
- });
2182
-
2183
- test("filters plugin-defined pane templates directly from the root query", async () => {
2184
- testSetup = await testRender(<CommandBarHarness query="chat" />, {
2185
- width: 100,
2186
- height: 18,
2187
- });
2188
-
2189
- await testSetup.renderOnce();
2190
-
2191
- const frame = testSetup.captureCharFrame();
2192
- expect(frame).toContain("Chat");
2193
- expect(frame).not.toContain("float");
2194
- });
2195
-
2196
- test("shows pane shortcuts in the default browse results", async () => {
2197
- testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
2198
- width: 100,
2199
- height: 24,
2200
- });
2201
-
2202
- await testSetup.renderOnce();
2203
-
2204
- const frame = testSetup.captureCharFrame();
2205
- expect(frame).toContain("Panes");
2206
- expect(frame).toContain("Quote Monitor");
2207
- expect(frame).toContain("QQ");
2208
- });
2209
-
2210
- test("matches direct pane shortcut queries", async () => {
2211
- testSetup = await testRender(<CommandBarHarness query="QQ MSFT" selectedTicker="AAPL" />, {
2212
- width: 100,
2213
- height: 18,
2214
- });
2215
-
2216
- await testSetup.renderOnce();
2217
-
2218
- const frame = testSetup.captureCharFrame();
2219
- expect(frame).toContain("Quote Monitor");
2220
- expect(frame).toContain("QQ");
2221
- });
2222
-
2223
- test("shows pane templates that share the same shortcut", async () => {
2224
- testSetup = await testRender(<CommandBarHarness
2225
- query="DUP"
2226
- configurePluginRegistry={(pluginRegistry) => {
2227
- const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
2228
- paneTemplates.set("first-duplicate-pane", {
2229
- id: "first-duplicate-pane",
2230
- paneId: "first-duplicate",
2231
- label: "First Duplicate",
2232
- description: "First pane using a shared shortcut",
2233
- shortcut: { prefix: "DUP" },
2234
- });
2235
- paneTemplates.set("second-duplicate-pane", {
2236
- id: "second-duplicate-pane",
2237
- paneId: "second-duplicate",
2238
- label: "Second Duplicate",
2239
- description: "Second pane using a shared shortcut",
2240
- shortcut: { prefix: "DUP" },
2241
- });
2242
- }}
2243
- />, {
2244
- width: 100,
2245
- height: 18,
2246
- });
2247
-
2248
- await testSetup.renderOnce();
2249
-
2250
- const frame = testSetup.captureCharFrame();
2251
- expect(frame).toContain("First Duplicate");
2252
- expect(frame).toContain("Second Duplicate");
2253
- });
2254
-
2255
- test("does not treat no-argument shortcut prefixes as typed pane names", async () => {
2256
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2257
-
2258
- testSetup = await testRender(<CommandBarHarness
2259
- query="Top News"
2260
- configurePluginRegistry={(pluginRegistry) => {
2261
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2262
- created.push({ templateId, options });
2263
- };
2264
- const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
2265
- paneTemplates.set("market-movers-pane", {
2266
- id: "market-movers-pane",
2267
- paneId: "market-movers",
2268
- label: "Market Movers",
2269
- description: "Track market gainers and losers",
2270
- shortcut: { prefix: "MOST" },
2271
- });
2272
- paneTemplates.set("news-top-pane", {
2273
- id: "news-top-pane",
2274
- paneId: "news-top",
2275
- label: "Top News",
2276
- description: "Curated top market stories ranked by importance",
2277
- shortcut: { prefix: "TOP" },
2278
- });
2279
- }}
2280
- />, {
2281
- width: 100,
2282
- height: 18,
2283
- });
2284
-
2285
- await testSetup.renderOnce();
2286
- expect(testSetup.captureCharFrame()).toContain("Top News");
2287
-
2288
- await act(async () => {
2289
- testSetup!.mockInput.pressEnter();
2290
- await Bun.sleep(0);
2291
- await testSetup!.renderOnce();
2292
- });
2293
-
2294
- expect(created).toEqual([{ templateId: "news-top-pane", options: undefined }]);
2295
- });
2296
-
2297
- test("surfaces ticker-required pane templates by name without an active ticker", async () => {
2298
- testSetup = await testRender(<CommandBarHarness
2299
- query="Options"
2300
- extraTickers={[makeTicker("^RUT", "Chicago Board Options Exchange Russell 2000")]}
2301
- configurePluginRegistry={(pluginRegistry) => {
2302
- const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
2303
- paneTemplates.set("options-pane", {
2304
- id: "options-pane",
2305
- paneId: "options",
2306
- label: "Options",
2307
- description: "Options chain for the selected ticker",
2308
- keywords: ["options", "chain", "calls", "puts"],
2309
- shortcut: { prefix: "OMON", argPlaceholder: "ticker", argKind: "ticker" },
2310
- canCreate: (context: any, options?: PaneTemplateCreateOptions) => (
2311
- context.activeTicker !== null || !!options?.arg
2312
- ),
2313
- });
2314
- }}
2315
- />, {
2316
- width: 100,
2317
- height: 18,
2318
- });
2319
-
2320
- await testSetup.renderOnce();
2321
-
2322
- let frame = testSetup.captureCharFrame();
2323
- expect(frame).toContain("Options");
2324
- expect(frame).toContain("OMON");
2325
-
2326
- await act(async () => {
2327
- testSetup!.mockInput.pressEnter();
2328
- await testSetup!.renderOnce();
2329
- });
2330
-
2331
- frame = testSetup.captureCharFrame();
2332
- expect(frame).toContain("Back");
2333
- expect(frame).toContain("Ticker");
2334
- });
2335
-
2336
- test("ranks exact ticker symbols before longer ticker prefixes in root search", async () => {
2337
- testSetup = await testRender(<CommandBarHarness
2338
- query="amd"
2339
- extraTickers={[
2340
- makeTicker("AMDL", "GraniteShares 2x Long AMD Daily ETF"),
2341
- makeTicker("AMD", "Advanced Micro Devices"),
2342
- ]}
2343
- />, {
2344
- width: 100,
2345
- height: 18,
2346
- });
2347
-
2348
- await testSetup.renderOnce();
2349
-
2350
- const tickerLines = testSetup.captureCharFrame()
2351
- .split("\n")
2352
- .filter((line) => /\bAMD\b|\bAMDL\b/.test(line));
2353
- expect(tickerLines[0]).toMatch(/\bAMD\b/);
2354
- expect(tickerLines[0]).not.toMatch(/\bAMDL\b/);
2355
- });
2356
-
2357
- test("opens plugin command wizards inline inside the command bar", async () => {
2358
- let submittedValues: Record<string, string> | undefined;
2359
-
2360
- testSetup = await testRender(<CommandBarHarness
2361
- query="Plugin Login"
2362
- configurePluginRegistry={(pluginRegistry) => {
2363
- (pluginRegistry.commands as Map<string, any>).set("plugin:login", {
2364
- id: "plugin:login",
2365
- label: "Plugin Login",
2366
- description: "Authenticate without leaving the command bar",
2367
- category: "config",
2368
- wizard: [
2369
- { key: "username", label: "Username", type: "text", placeholder: "vince" },
2370
- { key: "password", label: "Password", type: "password", placeholder: "secret" },
2371
- { key: "_validate", label: "Validating", type: "info", body: ["Validating…", "Connected."] },
2372
- ],
2373
- execute: async (values?: Record<string, string>) => {
2374
- submittedValues = values;
2375
- },
2376
- } as any);
2377
- pluginRegistry.getCommandPluginId = (commandId: string) => (
2378
- commandId === "plugin:login" ? "notes" : "news"
2379
- );
2380
- }}
2381
- />, {
2382
- width: 100,
2383
- height: 24,
2384
- });
2385
-
2386
- await testSetup.renderOnce();
2387
- await act(async () => {
2388
- testSetup!.mockInput.pressEnter();
2389
- await testSetup!.renderOnce();
2390
- });
2391
-
2392
- let frame = testSetup.captureCharFrame();
2393
- expect(frame).toContain("Plugin Login");
2394
- expect(frame).toContain("Username");
2395
- expect(frame).toContain("Password");
2396
-
2397
- await act(async () => {
2398
- await testSetup!.mockInput.typeText("vince");
2399
- await testSetup!.renderOnce();
2400
- });
2401
- await act(async () => {
2402
- testSetup!.mockInput.pressTab();
2403
- await testSetup!.renderOnce();
2404
- });
2405
- await act(async () => {
2406
- await testSetup!.mockInput.typeText("secret");
2407
- await testSetup!.renderOnce();
2408
- });
2409
-
2410
- frame = await waitForFrameToContain("******");
2411
- expect(frame).toContain("******");
2412
- expect(frame).not.toContain("secret");
2413
- expectSingleBackControl(frame);
2414
-
2415
- await act(async () => {
2416
- testSetup!.mockInput.pressEnter();
2417
- await Bun.sleep(0);
2418
- await testSetup!.renderOnce();
2419
- });
2420
-
2421
- expect(submittedValues).toEqual({
2422
- username: "vince",
2423
- password: "secret",
2424
- });
2425
- });
2426
-
2427
- test("does not duplicate placeholder text as workflow field help", async () => {
2428
- testSetup = await testRender(<CommandBarHarness
2429
- query="Duplicate Form"
2430
- configurePluginRegistry={(pluginRegistry) => {
2431
- (pluginRegistry.commands as Map<string, any>).set("plugin:duplicate-form", {
2432
- id: "plugin:duplicate-form",
2433
- label: "Duplicate Form",
2434
- description: "Check workflow field copy",
2435
- category: "config",
2436
- wizard: [
2437
- { key: "name", label: "Name", type: "text", placeholder: "Profile name" },
2438
- { key: "token", label: "Token", type: "text", placeholder: "repeat marker", body: ["repeat marker"] },
2439
- ],
2440
- execute: async () => {},
2441
- } as any);
2442
- pluginRegistry.getCommandPluginId = () => "notes";
2443
- }}
2444
- />, {
2445
- width: 100,
2446
- height: 24,
2447
- });
2448
-
2449
- await testSetup.renderOnce();
2450
- await act(async () => {
2451
- testSetup!.mockInput.pressEnter();
2452
- await testSetup!.renderOnce();
2453
- });
2454
-
2455
- const frame = testSetup.captureCharFrame();
2456
- expect(frame.match(/repeat marker/g)?.length ?? 0).toBe(1);
2457
- });
2458
-
2459
- test("does not treat backspace as Back inside workflow text fields", async () => {
2460
- testSetup = await testRender(<CommandBarHarness
2461
- query="Plugin Login"
2462
- configurePluginRegistry={(pluginRegistry) => {
2463
- (pluginRegistry.commands as Map<string, any>).set("plugin:login", {
2464
- id: "plugin:login",
2465
- label: "Plugin Login",
2466
- description: "Authenticate without leaving the command bar",
2467
- category: "config",
2468
- wizard: [
2469
- { key: "username", label: "Username", type: "text" },
2470
- ],
2471
- execute: async () => {},
2472
- } as any);
2473
- pluginRegistry.getCommandPluginId = () => "notes";
2474
- }}
2475
- />, {
2476
- width: 100,
2477
- height: 24,
2478
- });
2479
-
2480
- await testSetup.renderOnce();
2481
- await act(async () => {
2482
- testSetup!.mockInput.pressEnter();
2483
- await testSetup!.renderOnce();
2484
- });
2485
-
2486
- await act(async () => {
2487
- await testSetup!.mockInput.typeText("vince");
2488
- await testSetup!.renderOnce();
2489
- });
2490
- await act(async () => {
2491
- testSetup!.mockInput.pressBackspace();
2492
- await testSetup!.renderOnce();
2493
- });
2494
-
2495
- const frame = testSetup.captureCharFrame();
2496
- expect(frame).toContain("Plugin Login");
2497
- expect(frame).toContain("vinc");
2498
- expect(frame).toContain("Back");
2499
- });
2500
-
2501
- test("QQ MSFT executes directly without opening a secondary workflow", async () => {
2502
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2503
-
2504
- testSetup = await testRender(<CommandBarHarness
2505
- query="QQ MSFT"
2506
- selectedTicker="AAPL"
2507
- configurePluginRegistry={(pluginRegistry) => {
2508
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2509
- created.push({ templateId, options });
2510
- };
2511
- }}
2512
- />, {
2513
- width: 100,
2514
- height: 20,
2515
- });
2516
-
2517
- await testSetup.renderOnce();
2518
- await act(async () => {
2519
- testSetup!.mockInput.pressEnter();
2520
- await Bun.sleep(0);
2521
- await testSetup!.renderOnce();
2522
- });
2523
-
2524
- expect(created).toEqual([{
2525
- templateId: "quote-monitor-pane",
2526
- options: {
2527
- arg: "MSFT",
2528
- symbols: ["MSFT"],
2529
- },
2530
- }]);
2531
- });
2532
-
2533
- test("QQ AAPL,MSFT creates a multi-symbol quote monitor directly", async () => {
2534
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2535
-
2536
- testSetup = await testRender(<CommandBarHarness
2537
- query="QQ AAPL,MSFT"
2538
- selectedTicker="AAPL"
2539
- configurePluginRegistry={(pluginRegistry) => {
2540
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2541
- created.push({ templateId, options });
2542
- };
2543
- }}
2544
- />, {
2545
- width: 100,
2546
- height: 20,
2547
- });
2548
-
2549
- await testSetup.renderOnce();
2550
- await act(async () => {
2551
- testSetup!.mockInput.pressEnter();
2552
- await Bun.sleep(0);
2553
- await testSetup!.renderOnce();
2554
- });
2555
-
2556
- expect(created).toEqual([{
2557
- templateId: "quote-monitor-pane",
2558
- options: {
2559
- arg: "AAPL,MSFT",
2560
- symbols: ["AAPL", "MSFT"],
2561
- },
2562
- }]);
2563
- });
2564
-
2565
- test("CMP AAPL,MSFT creates the comparison chart directly", async () => {
2566
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2567
-
2568
- testSetup = await testRender(<CommandBarHarness
2569
- query="CMP AAPL,MSFT"
2570
- configurePluginRegistry={(pluginRegistry) => {
2571
- (pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
2572
- id: "comparison-chart",
2573
- name: "Comparison Chart",
2574
- component: () => null,
2575
- defaultPosition: "right",
2576
- });
2577
- (pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
2578
- id: "comparison-chart-pane",
2579
- paneId: "comparison-chart",
2580
- label: "Comparison Chart",
2581
- description: "Compare multiple symbols in one pane",
2582
- shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
2583
- });
2584
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2585
- created.push({ templateId, options });
2586
- };
2587
- }}
2588
- />, {
2589
- width: 100,
2590
- height: 20,
2591
- });
2592
-
2593
- await testSetup.renderOnce();
2594
- await act(async () => {
2595
- testSetup!.mockInput.pressEnter();
2596
- await Bun.sleep(0);
2597
- await testSetup!.renderOnce();
2598
- });
2599
-
2600
- expect(created).toEqual([{
2601
- templateId: "comparison-chart-pane",
2602
- options: {
2603
- arg: "AAPL,MSFT",
2604
- symbols: ["AAPL", "MSFT"],
2605
- },
2606
- }]);
2607
- });
2608
-
2609
- test("CMP AAPL, opens inline completion when the ticker list is incomplete", async () => {
2610
- testSetup = await testRender(<CommandBarHarness
2611
- query="CMP AAPL,"
2612
- configurePluginRegistry={(pluginRegistry) => {
2613
- (pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
2614
- id: "comparison-chart",
2615
- name: "Comparison Chart",
2616
- component: () => null,
2617
- defaultPosition: "right",
2618
- });
2619
- (pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
2620
- id: "comparison-chart-pane",
2621
- paneId: "comparison-chart",
2622
- label: "Comparison Chart",
2623
- description: "Compare multiple symbols in one pane",
2624
- shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
2625
- });
2626
- }}
2627
- />, {
2628
- width: 100,
2629
- height: 20,
2630
- });
2631
-
2632
- await testSetup.renderOnce();
2633
- await act(async () => {
2634
- testSetup!.mockInput.pressEnter();
2635
- await testSetup!.renderOnce();
2636
- });
2637
-
2638
- const frame = testSetup.captureCharFrame();
2639
- expect(frame).toContain("Comparison Chart");
2640
- expect(frame).toContain("Tickers");
2641
- expect(frame).toContain("AAPL,");
2642
- });
2643
-
2644
- test("CMP with one resolved ticker opens inline completion instead of creating a one-symbol chart", async () => {
2645
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2646
-
2647
- testSetup = await testRender(<CommandBarHarness
2648
- query="CMP AMD"
2649
- extraTickers={[makeTicker("AMD", "Advanced Micro Devices")]}
2650
- configurePluginRegistry={(pluginRegistry) => {
2651
- (pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
2652
- id: "comparison-chart",
2653
- name: "Comparison Chart",
2654
- component: () => null,
2655
- defaultPosition: "right",
2656
- });
2657
- (pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
2658
- id: "comparison-chart-pane",
2659
- paneId: "comparison-chart",
2660
- label: "Comparison Chart",
2661
- description: "Compare multiple symbols in one pane",
2662
- shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
2663
- wizard: [{ key: "tickers", label: "Tickers", type: "text" }],
2664
- canCreate: (_context: unknown, options?: { symbols?: string[] | null }) => !options?.symbols || options.symbols.length >= 2,
2665
- });
2666
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2667
- created.push({ templateId, options });
2668
- };
2669
- }}
2670
- />, {
2671
- width: 100,
2672
- height: 20,
2673
- });
2674
-
2675
- await testSetup.renderOnce();
2676
- await act(async () => {
2677
- testSetup!.mockInput.pressEnter();
2678
- await Bun.sleep(0);
2679
- await testSetup!.renderOnce();
2680
- });
2681
-
2682
- const frame = await waitForFrameToContain("Tickers");
2683
- expect(created).toEqual([]);
2684
- expect(frame).toContain("Comparison Chart");
2685
- expect(frame).toContain("AMD");
2686
- });
2687
-
2688
- test("AI <prompt> opens the inline workflow and prefills the textarea prompt", async () => {
2689
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2690
-
2691
- testSetup = await testRender(<CommandBarHarness
2692
- query="AI quality compounders"
2693
- configurePluginRegistry={(pluginRegistry) => {
2694
- (pluginRegistry.panes as Map<string, any>).set("ai-screener", {
2695
- id: "ai-screener",
2696
- name: "AI Screener",
2697
- component: () => null,
2698
- defaultPosition: "right",
2699
- defaultMode: "floating",
2700
- });
2701
- (pluginRegistry.paneTemplates as Map<string, any>).set("new-ai-screener-pane", {
2702
- id: "new-ai-screener-pane",
2703
- paneId: "ai-screener",
2704
- label: "AI Screener",
2705
- description: "Create a prompt-driven screener pane.",
2706
- shortcut: { prefix: "AI", argPlaceholder: "prompt", argKind: "text" },
2707
- wizard: [
2708
- {
2709
- key: "providerId",
2710
- label: "AI Provider",
2711
- type: "select",
2712
- defaultValue: "claude",
2713
- options: [{ label: "Claude", value: "claude" }],
2714
- },
2715
- {
2716
- key: "prompt",
2717
- label: "Screener Prompt",
2718
- type: "textarea",
2719
- },
2720
- ],
2721
- });
2722
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2723
- created.push({ templateId, options });
2724
- };
2725
- }}
2726
- />, {
2727
- width: 100,
2728
- height: 24,
2729
- });
2730
-
2731
- await testSetup.renderOnce();
2732
- await act(async () => {
2733
- testSetup!.mockInput.pressEnter();
2734
- await testSetup!.renderOnce();
2735
- });
2736
-
2737
- const frame = testSetup.captureCharFrame();
2738
- expect(frame).toContain("AI Screener");
2739
- expect(frame).toContain("AI Provider");
2740
- expect(frame).toContain("quality compounders");
2741
- expect(created).toEqual([]);
2742
- });
2743
-
2744
- test("submits typed AI screener prompts from the textarea field", async () => {
2745
- const created: Array<{ templateId: string; options?: PaneTemplateCreateOptions }> = [];
2746
-
2747
- testSetup = await testRender(<CommandBarHarness
2748
- query="AI"
2749
- configurePluginRegistry={(pluginRegistry) => {
2750
- (pluginRegistry.panes as Map<string, any>).set("ai-screener", {
2751
- id: "ai-screener",
2752
- name: "AI Screener",
2753
- component: () => null,
2754
- defaultPosition: "right",
2755
- defaultMode: "floating",
2756
- });
2757
- (pluginRegistry.paneTemplates as Map<string, any>).set("new-ai-screener-pane", {
2758
- id: "new-ai-screener-pane",
2759
- paneId: "ai-screener",
2760
- label: "AI Screener",
2761
- description: "Create a prompt-driven screener pane.",
2762
- shortcut: { prefix: "AI", argPlaceholder: "prompt", argKind: "text" },
2763
- wizard: [
2764
- {
2765
- key: "providerId",
2766
- label: "AI Provider",
2767
- type: "select",
2768
- defaultValue: "claude",
2769
- options: [{ label: "Claude", value: "claude" }],
2770
- },
2771
- {
2772
- key: "prompt",
2773
- label: "Screener Prompt",
2774
- type: "textarea",
2775
- },
2776
- ],
2777
- });
2778
- pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
2779
- created.push({ templateId, options });
2780
- };
2781
- }}
2782
- />, {
2783
- width: 100,
2784
- height: 24,
2785
- });
2786
-
2787
- await testSetup.renderOnce();
2788
- await act(async () => {
2789
- testSetup!.mockInput.pressEnter();
2790
- await testSetup!.renderOnce();
2791
- });
2792
- await act(async () => {
2793
- testSetup!.mockInput.pressTab();
2794
- await testSetup!.renderOnce();
2795
- });
2796
- await act(async () => {
2797
- await testSetup!.mockInput.typeText("humanoid robot suppliers");
2798
- await testSetup!.renderOnce();
2799
- });
2800
- await clickFrameText("Create Pane");
2801
- await act(async () => {
2802
- await Bun.sleep(0);
2803
- await testSetup!.renderOnce();
2804
- });
2805
-
2806
- expect(created).toEqual([{
2807
- templateId: "new-ai-screener-pane",
2808
- options: {
2809
- arg: "humanoid robot suppliers",
2810
- values: {
2811
- providerId: "claude",
2812
- prompt: "humanoid robot suppliers",
2813
- },
2814
- },
2815
- }]);
2816
- });
2817
-
2818
- test("edits pane settings inline inside the command bar", async () => {
2819
- const appliedValues: Array<{ paneId: string; key: string; value: unknown }> = [];
2820
-
2821
- testSetup = await testRender(<CommandBarHarness
2822
- query="PS"
2823
- configureState={(state) => ({
2824
- ...state,
2825
- focusedPaneId: "quote-monitor:main",
2826
- })}
2827
- hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
2828
- configurePluginRegistry={(pluginRegistry) => {
2829
- pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
2830
- key: "symbol",
2831
- label: "Symbol",
2832
- type: "text",
2833
- description: "Ticker symbol to track",
2834
- }]);
2835
- pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
2836
- appliedValues.push({ paneId, key: field.key, value });
2837
- };
2838
- }}
2839
- />, {
2840
- width: 100,
2841
- height: 20,
2842
- });
2843
-
2844
- await testSetup.renderOnce();
2845
-
2846
- await act(async () => {
2847
- testSetup!.mockInput.pressEnter();
2848
- await testSetup!.renderOnce();
2849
- });
2850
- let frame = testSetup.captureCharFrame();
2851
- expect(frame).toContain("Quote Monitor Settings");
2852
- expect(frame).toContain("Symbol");
2853
-
2854
- await act(async () => {
2855
- testSetup!.mockInput.pressEnter();
2856
- await testSetup!.renderOnce();
2857
- });
2858
- frame = testSetup.captureCharFrame();
2859
- await clickFrameText("Symbol");
2860
- frame = testSetup.captureCharFrame();
2861
- expect(frame).toContain("Apply");
2862
- expect(frame).toContain("Symbol");
2863
-
2864
- await act(async () => {
2865
- await testSetup!.mockInput.typeText("MSFT");
2866
- await testSetup!.renderOnce();
2867
- });
2868
- await act(async () => {
2869
- testSetup!.mockInput.pressEnter();
2870
- await Bun.sleep(0);
2871
- await testSetup!.renderOnce();
2872
- });
2873
-
2874
- expect(appliedValues).toEqual([{
2875
- paneId: "quote-monitor:main",
2876
- key: "symbol",
2877
- value: "MSFT",
2878
- }]);
2879
- });
2880
-
2881
- test("opens focused pane settings directly from root search", async () => {
2882
- const appliedValues: Array<{ paneId: string; key: string; value: unknown }> = [];
2883
-
2884
- testSetup = await testRender(<CommandBarHarness
2885
- query="ticker symbol"
2886
- configureState={(state) => ({
2887
- ...state,
2888
- focusedPaneId: "quote-monitor:main",
2889
- })}
2890
- hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
2891
- configurePluginRegistry={(pluginRegistry) => {
2892
- pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
2893
- key: "symbol",
2894
- label: "Symbol",
2895
- type: "text",
2896
- description: "Ticker symbol to track",
2897
- }]);
2898
- pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
2899
- appliedValues.push({ paneId, key: field.key, value });
2900
- };
2901
- }}
2902
- />, {
2903
- width: 100,
2904
- height: 20,
2905
- });
2906
-
2907
- await testSetup.renderOnce();
2908
-
2909
- let frame = testSetup.captureCharFrame();
2910
- expect(frame).toContain("Quote Monitor Settings");
2911
- expect(frame).toContain("Symbol");
2912
-
2913
- await act(async () => {
2914
- testSetup!.mockInput.pressEnter();
2915
- await testSetup!.renderOnce();
2916
- });
2917
-
2918
- frame = testSetup.captureCharFrame();
2919
- expect(frame).toContain("Apply");
2920
- expect(frame).toContain("Symbol");
2921
-
2922
- await act(async () => {
2923
- await testSetup!.mockInput.typeText("MSFT");
2924
- await testSetup!.renderOnce();
2925
- });
2926
- await act(async () => {
2927
- testSetup!.mockInput.pressEnter();
2928
- await Bun.sleep(0);
2929
- await testSetup!.renderOnce();
2930
- });
2931
-
2932
- expect(appliedValues).toEqual([{
2933
- paneId: "quote-monitor:main",
2934
- key: "symbol",
2935
- value: "MSFT",
2936
- }]);
2937
- });
2938
-
2939
- test("uses backspace as back only when a pane-settings route query is empty", async () => {
2940
- testSetup = await testRender(<CommandBarHarness
2941
- query="PS"
2942
- configureState={(state) => ({
2943
- ...state,
2944
- focusedPaneId: "quote-monitor:main",
2945
- })}
2946
- hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
2947
- configurePluginRegistry={(pluginRegistry) => {
2948
- pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
2949
- key: "symbol",
2950
- label: "Symbol",
2951
- type: "text",
2952
- description: "Ticker symbol to track",
2953
- }]);
2954
- }}
2955
- />, {
2956
- width: 100,
2957
- height: 20,
2958
- });
2959
-
2960
- await testSetup.renderOnce();
2961
-
2962
- await act(async () => {
2963
- testSetup!.mockInput.pressEnter();
2964
- await testSetup!.renderOnce();
2965
- });
2966
- await renderFrames();
2967
-
2968
- let frame = testSetup.captureCharFrame();
2969
- expect(frame).toContain("Quote Monitor Settings");
2970
-
2971
- await act(async () => {
2972
- await testSetup!.mockInput.typeText("s");
2973
- await testSetup!.renderOnce();
2974
- });
2975
-
2976
- frame = testSetup.captureCharFrame();
2977
- expect(frame).toContain("Quote Monitor Settings");
2978
-
2979
- await emitKeypress(testSetup, { name: "backspace", sequence: "\b" });
2980
-
2981
- frame = testSetup.captureCharFrame();
2982
- expect(frame).toContain("Quote Monitor Settings");
2983
-
2984
- await act(async () => {
2985
- testSetup!.mockInput.pressBackspace();
2986
- await testSetup!.renderOnce();
2987
- });
2988
- await renderFrames();
2989
- await act(async () => {
2990
- testSetup!.mockInput.pressBackspace();
2991
- await testSetup!.renderOnce();
2992
- });
2993
- await renderFrames();
2994
-
2995
- frame = testSetup.captureCharFrame();
2996
- expect(frame).not.toContain("Quote Monitor Settings");
2997
- expect(frame).not.toContain("Back Pane Settings");
2998
- });
2999
-
3000
- test("renders pane-setting multi-select pickers only once inside the command bar", async () => {
3001
- testSetup = await testRender(<CommandBarHarness
3002
- query="PS"
3003
- configureState={(state) => ({
3004
- ...state,
3005
- focusedPaneId: "quote-monitor:main",
3006
- })}
3007
- hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
3008
- configurePluginRegistry={(pluginRegistry) => {
3009
- pluginRegistry.resolvePaneSettings = () => makeQuoteMonitorPaneSettingsDescriptor(pluginRegistry, [{
3010
- key: "columns",
3011
- label: "Columns",
3012
- type: "ordered-multi-select",
3013
- description: "Visible columns",
3014
- options: [
3015
- { value: "volume", label: "AAA", description: "Volume column" },
3016
- { value: "spread", label: "BBB", description: "Spread column" },
3017
- { value: "beta", label: "CCC", description: "Beta column" },
3018
- ],
3019
- }], { columns: ["volume", "spread"] });
3020
- }}
3021
- />, {
3022
- width: 100,
3023
- height: 20,
3024
- });
3025
-
3026
- await testSetup.renderOnce();
3027
-
3028
- await act(async () => {
3029
- testSetup!.mockInput.pressEnter();
3030
- await testSetup!.renderOnce();
3031
- });
3032
- await clickFrameText("Columns");
3033
-
3034
- const frame = await waitForFrameToContain("Done");
3035
- expect(frame).toContain("Columns");
3036
- expect(frame).toContain("Done");
3037
- expect(frame).not.toContain("Options");
3038
- expectSingleBackControl(frame);
3039
- });
3040
-
3041
- test("matches the IBKR trading shortcut when a gateway profile exists", async () => {
3042
- testSetup = await testRender(<CommandBarHarness
3043
- query="IBKR"
3044
- configureConfig={(config) => ({
3045
- ...config,
3046
- brokerInstances: [{
3047
- id: "ibkr-paper",
3048
- brokerType: "ibkr",
3049
- label: "Paper",
3050
- connectionMode: "gateway",
3051
- config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4002, clientId: 1 } },
3052
- enabled: true,
3053
- }],
3054
- })}
3055
- />, {
3056
- width: 100,
3057
- height: 18,
3058
- });
3059
-
3060
- await testSetup.renderOnce();
3061
-
3062
- const frame = testSetup.captureCharFrame();
3063
- expect(frame).toContain("IBKR Trading");
3064
- expect(frame).toContain("IBKR");
3065
- });
3066
-
3067
- test("matches the direct help command", async () => {
3068
- const openedPanes: string[] = [];
3069
- testSetup = await testRender(<CommandBarHarness
3070
- query="help"
3071
- configurePluginRegistry={(pluginRegistry) => {
3072
- pluginRegistry.showPane = (paneId: string) => {
3073
- openedPanes.push(paneId);
3074
- };
3075
- }}
3076
- />, {
3077
- width: 80,
3078
- height: 18,
3079
- });
3080
-
3081
- await testSetup.renderOnce();
3082
-
3083
- const frame = testSetup.captureCharFrame();
3084
- expect(frame).toContain("Help");
3085
- expect(frame).toContain("Navigation");
3086
-
3087
- await act(async () => {
3088
- testSetup!.mockInput.pressEnter();
3089
- await testSetup!.renderOnce();
3090
- });
3091
-
3092
- expect(openedPanes).toEqual(["help"]);
3093
- });
3094
-
3095
- test("skips pane templates whose canCreate throws", async () => {
3096
- testSetup = await testRender(<CommandBarHarness
3097
- query=""
3098
- selectedTicker="AAPL"
3099
- configurePluginRegistry={(pluginRegistry) => {
3100
- (pluginRegistry.paneTemplates as Map<string, any>).set("broken-pane", {
3101
- id: "broken-pane",
3102
- paneId: "chat",
3103
- label: "Broken Pane",
3104
- description: "Should not crash the command bar",
3105
- shortcut: { prefix: "IBKR" },
3106
- canCreate: () => {
3107
- throw new ReferenceError("getIbkrInstances is not defined");
3108
- },
3109
- });
3110
- }}
3111
- />, {
3112
- width: 100,
3113
- height: 24,
3114
- });
3115
-
3116
- await testSetup.renderOnce();
3117
-
3118
- const frame = testSetup.captureCharFrame();
3119
- expect(frame).toContain("Quote Monitor");
3120
- expect(frame).not.toContain("Broken Pane");
3121
- });
3122
-
3123
- test("groups ticker search sections and keeps saved matches above looser provider results", async () => {
3124
- testSetup = await testRender(
3125
- <CommandBarHarness
3126
- query="DES appl"
3127
- dataProvider={makeDataProvider(async () => [
3128
- { providerId: "yahoo", symbol: "IVSX", name: "Invsivx Holdings", exchange: "NYSE", type: "ETF" },
3129
- { providerId: "yahoo", symbol: "AAPL", name: "Apple Inc", exchange: "NASDAQ", type: "EQUITY" },
3130
- { providerId: "yahoo", symbol: "AMAT", name: "Applied Materials", exchange: "NASDAQ", type: "EQUITY" },
3131
- { providerId: "yahoo", symbol: "AAOI", name: "Applied Optoelectronics", exchange: "NASDAQ", type: "EQUITY" },
3132
- { providerId: "yahoo", symbol: "APP", name: "AppLovin Corp", exchange: "NASDAQ", type: "EQUITY" },
3133
- ])}
3134
- />,
3135
- { width: 80, height: 24 },
3136
- );
3137
-
3138
- await testSetup.renderOnce();
3139
- await waitForFrameToContain("AAOI");
3140
-
3141
- const frame = testSetup.captureCharFrame();
3142
- const rows = frame.split("\n");
3143
- const savedHeadings = frame.split("\n").filter((line) => line.trim() === "Saved");
3144
- const otherListingsHeadings = frame.split("\n").filter((line) => line.trim() === "Other Listings");
3145
- const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL") && line.includes("NASDAQ"));
3146
- const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP") && line.includes("NASDAQ"));
3147
- expect(savedHeadings).toHaveLength(1);
3148
- expect(otherListingsHeadings).toHaveLength(1);
3149
- expect(aaplRow).toBeGreaterThanOrEqual(0);
3150
- expect(appRow).toBeGreaterThanOrEqual(0);
3151
- expect(aaplRow).toBeLessThan(appRow);
3152
- expect(frame).toMatchSnapshot();
3153
- });
3154
-
3155
- test("renders form-layout wizard fields together on one screen", async () => {
3156
- testSetup = await testRender(
3157
- <CommandBarHarness
3158
- query="auth login"
3159
- live
3160
- configurePluginRegistry={(pluginRegistry) => {
3161
- (pluginRegistry.commands as Map<string, any>).set("auth-login", {
3162
- id: "auth-login",
3163
- label: "Auth Login",
3164
- description: "Log in to your account",
3165
- keywords: ["login", "auth"],
3166
- category: "config",
3167
- wizardLayout: "form",
3168
- wizard: [
3169
- { key: "email", label: "Email", type: "text", placeholder: "email@example.com" },
3170
- { key: "password", label: "Password", type: "password", placeholder: "Your password" },
3171
- ],
3172
- execute: async () => {},
3173
- } as any);
3174
- }}
3175
- />,
3176
- { width: 80, height: 24 },
3177
- );
3178
-
3179
- await testSetup.renderOnce();
3180
- await clickFrameText("Auth Login");
3181
- await act(async () => {
3182
- await testSetup!.renderOnce();
3183
- });
3184
-
3185
- let frame = testSetup.captureCharFrame();
3186
- expect(frame).toContain("Back");
3187
- expect(frame).toContain("Email");
3188
- expect(frame).toContain("Password");
3189
- expect(frame).toContain("Your password");
3190
- expectSingleBackControl(frame);
3191
- });
3192
-
3193
- test("submits single-field form-layout wizards", async () => {
3194
- const submitted: Array<Record<string, string> | undefined> = [];
3195
-
3196
- testSetup = await testRender(
3197
- <CommandBarHarness
3198
- query="workspace"
3199
- live
3200
- configurePluginRegistry={(pluginRegistry) => {
3201
- (pluginRegistry.commands as Map<string, any>).set("new-workspace", {
3202
- id: "new-workspace",
3203
- label: "Workspace",
3204
- description: "Create a workspace",
3205
- keywords: ["workspace"],
3206
- category: "config",
3207
- wizardLayout: "form",
3208
- wizard: [
3209
- { key: "name", label: "Name", type: "text", placeholder: "Research" },
3210
- ],
3211
- execute: async (values?: Record<string, string>) => {
3212
- submitted.push(values);
3213
- },
3214
- } as any);
3215
- }}
3216
- />,
3217
- { width: 80, height: 24 },
3218
- );
3219
-
3220
- await testSetup.renderOnce();
3221
-
3222
- await act(async () => {
3223
- testSetup!.mockInput.pressEnter();
3224
- await Bun.sleep(0);
3225
- await testSetup!.renderOnce();
3226
- await testSetup!.renderOnce();
3227
- });
3228
-
3229
- await act(async () => {
3230
- await testSetup!.mockInput.typeText("Research");
3231
- await testSetup!.renderOnce();
3232
- await testSetup!.renderOnce();
3233
- });
3234
-
3235
- await act(async () => {
3236
- testSetup!.mockInput.pressEnter();
3237
- await Bun.sleep(0);
3238
- await testSetup!.renderOnce();
3239
- await testSetup!.renderOnce();
3240
- });
3241
-
3242
- expect(submitted).toEqual([{ name: "Research" }]);
3243
- });
3244
- });