gloomberb 0.7.5 → 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 (1292) 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 +107 -1487
  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 +2 -76
  606. package/src/plugins/builtin/holders/index.tsx +3 -959
  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 +79 -4
  817. package/src/plugins/pane-manager.ts +57 -1061
  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 +165 -1001
  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 +1 -1
  888. package/src/renderers/electrobun/view/app-services.ts +4 -375
  889. package/src/renderers/electrobun/view/build-assets.ts +117 -0
  890. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +398 -0
  891. package/src/renderers/electrobun/view/config-host.ts +1 -1
  892. package/src/renderers/electrobun/view/context-menu.test.ts +10 -4
  893. package/src/renderers/electrobun/view/context-menu.ts +1 -1
  894. package/src/renderers/electrobun/view/data-table/dom.ts +211 -0
  895. package/src/renderers/electrobun/view/data-table/index.tsx +358 -0
  896. package/src/renderers/electrobun/view/data-table/row.tsx +335 -0
  897. package/src/renderers/electrobun/view/desktop/control-styles.ts +77 -0
  898. package/src/renderers/electrobun/view/desktop/controls.tsx +421 -0
  899. package/src/renderers/electrobun/view/desktop/list-view.tsx +200 -0
  900. package/src/renderers/electrobun/view/desktop/window/bridge.ts +70 -0
  901. package/src/renderers/electrobun/view/desktop/window/snapshot.test.ts +57 -0
  902. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +41 -0
  903. package/src/renderers/electrobun/view/fatal-screen.tsx +2 -2
  904. package/src/renderers/electrobun/view/host/box.tsx +160 -0
  905. package/src/renderers/electrobun/view/host/chart-surface.tsx +142 -0
  906. package/src/renderers/electrobun/view/host/input.tsx +294 -0
  907. package/src/renderers/electrobun/view/host/mouse.ts +139 -0
  908. package/src/renderers/electrobun/view/host/native.ts +49 -0
  909. package/src/renderers/electrobun/view/host/scroll-box.tsx +276 -0
  910. package/src/renderers/electrobun/view/host/style.ts +124 -0
  911. package/src/renderers/electrobun/view/host/tabs.tsx +255 -0
  912. package/src/renderers/electrobun/view/host/text.tsx +121 -0
  913. package/src/renderers/electrobun/view/http-fetch.ts +1 -1
  914. package/src/renderers/electrobun/view/input-host.tsx +12 -1
  915. package/src/renderers/electrobun/view/main.tsx +8 -3
  916. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +59 -0
  917. package/src/renderers/electrobun/view/native-stubs/chart/kitty-support.ts +9 -0
  918. package/src/renderers/electrobun/view/remote/asset-data-client.ts +201 -0
  919. package/src/renderers/electrobun/view/remote/persistence.ts +158 -0
  920. package/src/renderers/electrobun/view/remote/ticker-repository.ts +26 -0
  921. package/src/renderers/electrobun/view/styles.css +43 -0
  922. package/src/renderers/electrobun/view/ui-host.tsx +22 -1469
  923. package/src/renderers/opentui/chart-surface.tsx +4 -5
  924. package/src/renderers/opentui/host.tsx +30 -4
  925. package/src/renderers/opentui/image/loader.test.ts +25 -0
  926. package/src/renderers/opentui/image/loader.ts +105 -0
  927. package/src/renderers/opentui/image/surface.tsx +266 -0
  928. package/src/renderers/opentui/input-host.tsx +12 -1
  929. package/src/renderers/opentui/start.tsx +2 -2
  930. package/src/renderers/opentui/test-utils.tsx +2 -2
  931. package/src/renderers/opentui/ui-host.tsx +1 -1
  932. package/src/sources/gloomberb-cloud/index.test.ts +660 -0
  933. package/src/sources/gloomberb-cloud/index.ts +444 -0
  934. package/src/sources/gloomberb-cloud/news.ts +127 -0
  935. package/src/sources/gloomberb-cloud/normalizers.ts +354 -0
  936. package/src/sources/provider-errors.ts +8 -2
  937. package/src/sources/provider-router/batches.ts +170 -0
  938. package/src/sources/provider-router/brokers.ts +114 -0
  939. package/src/sources/provider-router/cache.ts +171 -0
  940. package/src/sources/provider-router/cached-financials.test.ts +498 -0
  941. package/src/sources/provider-router/document-routes.ts +153 -0
  942. package/src/sources/provider-router/financial-routes.ts +225 -0
  943. package/src/sources/provider-router/financials.ts +259 -0
  944. package/src/sources/provider-router/history.test.ts +383 -0
  945. package/src/sources/provider-router/history.ts +448 -0
  946. package/src/sources/provider-router/index.test.ts +1438 -0
  947. package/src/sources/provider-router/index.ts +389 -0
  948. package/src/sources/provider-router/news.ts +36 -0
  949. package/src/sources/provider-router/primary.ts +164 -0
  950. package/src/sources/provider-router/route-types.ts +34 -0
  951. package/src/sources/provider-router/routing.ts +58 -0
  952. package/src/sources/provider-router/search.ts +183 -0
  953. package/src/sources/provider-router/sources.ts +55 -0
  954. package/src/sources/provider-router/streaming.ts +92 -0
  955. package/src/sources/provider-router/supplemental.ts +373 -0
  956. package/src/sources/provider-router/test-support.ts +136 -0
  957. package/src/sources/sec-edgar/content.ts +211 -0
  958. package/src/sources/sec-edgar.ts +9 -217
  959. package/src/sources/yahoo-finance/financials.ts +339 -0
  960. package/src/sources/yahoo-finance/history.ts +106 -0
  961. package/src/sources/yahoo-finance/http.ts +100 -0
  962. package/src/sources/yahoo-finance/mappers.ts +409 -0
  963. package/src/sources/yahoo-finance/options.ts +134 -0
  964. package/src/sources/yahoo-finance/quote-summary.ts +210 -0
  965. package/src/sources/yahoo-finance/requests.ts +192 -0
  966. package/src/sources/yahoo-finance/snapshots.ts +236 -0
  967. package/src/sources/yahoo-finance/symbols.ts +102 -0
  968. package/src/sources/yahoo-finance/types.ts +167 -0
  969. package/src/sources/yahoo-finance.test.ts +116 -12
  970. package/src/sources/yahoo-finance.ts +115 -1408
  971. package/src/state/app/activity.test.ts +41 -0
  972. package/src/state/app/activity.ts +106 -0
  973. package/src/state/app/bootstrap.test.ts +498 -0
  974. package/src/state/app/bootstrap.ts +412 -0
  975. package/src/state/app/context/index.test.ts +564 -0
  976. package/src/state/app/context/index.test.tsx +193 -0
  977. package/src/state/app/context/index.tsx +476 -0
  978. package/src/state/app/context/selector.test.tsx +291 -0
  979. package/src/state/config-save-scheduler.ts +2 -6
  980. package/src/state/hooks/inline-tickers.test.tsx +123 -0
  981. package/src/state/hooks/inline-tickers.ts +212 -0
  982. package/src/state/hooks/quote-streaming.test.ts +31 -0
  983. package/src/state/hooks/quote-streaming.test.tsx +70 -0
  984. package/src/state/hooks/quote-streaming.ts +92 -0
  985. package/src/state/hooks/session-snapshot.ts +99 -0
  986. package/src/state/pane-scroll-registry.tsx +1 -1
  987. package/src/state/selectors-ui.ts +1 -1
  988. package/src/state/selectors.ts +1 -1
  989. package/src/test-support/plugin-persistence.ts +112 -0
  990. package/src/test-support/plugin-runtime.ts +2 -1
  991. package/src/theme/colors.ts +23 -9
  992. package/src/theme/themes.test.ts +15 -0
  993. package/src/tickers/list.test.ts +16 -0
  994. package/src/tickers/open-target.ts +77 -0
  995. package/src/tickers/search/index.test.ts +252 -0
  996. package/src/tickers/search/index.ts +367 -0
  997. package/src/tickers/search/ranking.ts +387 -0
  998. package/src/tickers/search/result.ts +27 -0
  999. package/src/tickers/search/types.ts +41 -0
  1000. package/src/tickers/search/upsert.ts +91 -0
  1001. package/src/tickers/tokenizer.test.ts +31 -0
  1002. package/src/tickers/tokenizer.ts +37 -0
  1003. package/src/types/broker.ts +12 -4
  1004. package/src/types/config.ts +42 -23
  1005. package/src/types/context-menu.ts +1 -1
  1006. package/src/types/data-provider.ts +2 -2
  1007. package/src/types/desktop-window.ts +1 -1
  1008. package/src/types/financials.ts +109 -4
  1009. package/src/types/news-source.ts +0 -1
  1010. package/src/types/persistence.ts +1 -1
  1011. package/src/types/plugin.ts +37 -21
  1012. package/src/types/ticker.ts +1 -0
  1013. package/src/types/trading.ts +23 -1
  1014. package/src/ui/context-menu.tsx +6 -5
  1015. package/src/ui/dialog.tsx +6 -3
  1016. package/src/ui/host.tsx +10 -10
  1017. package/src/ui/index.tsx +14 -33
  1018. package/src/updater/github-releases.test.ts +41 -0
  1019. package/src/updater/github-releases.ts +73 -0
  1020. package/src/updater.test.ts +21 -0
  1021. package/src/updater.ts +10 -3
  1022. package/src/utils/back-navigation.ts +2 -2
  1023. package/src/utils/cli-output.ts +4 -4
  1024. package/src/utils/currency-units.test.ts +23 -1
  1025. package/src/utils/currency-units.ts +56 -0
  1026. package/src/utils/datetime-format.ts +40 -0
  1027. package/src/utils/dom-screenshot.ts +2 -2
  1028. package/src/utils/double-escape-close.ts +1 -1
  1029. package/src/utils/exchanges.ts +2 -2
  1030. package/src/utils/format.ts +0 -7
  1031. package/src/utils/fuzzy-search.ts +1 -1
  1032. package/src/utils/inline-content-tokenizer.test.ts +11 -2
  1033. package/src/utils/inline-content-tokenizer.ts +33 -12
  1034. package/src/utils/keyboard.ts +1 -1
  1035. package/src/utils/link-tokenizer.ts +2 -2
  1036. package/src/utils/options.ts +1 -1
  1037. package/src/utils/price-history.test.ts +20 -0
  1038. package/src/utils/price-history.ts +24 -2
  1039. package/src/utils/relative-time.ts +8 -0
  1040. package/src/utils/selection-clipboard.ts +1 -1
  1041. package/src/utils/shortcut-labels.ts +2 -2
  1042. package/src/utils/sort-values.ts +17 -0
  1043. package/src/utils/terminal-input-reset.ts +1 -1
  1044. package/src/utils/text-wrap.ts +20 -0
  1045. package/src/version.ts +1 -1
  1046. package/src/components/chart/chart-controller.test.ts +0 -110
  1047. package/src/components/chart/chart-controller.ts +0 -650
  1048. package/src/components/chart/chart-data.ts +0 -350
  1049. package/src/components/chart/chart-indicator-selector.tsx +0 -75
  1050. package/src/components/chart/chart-pane-settings.ts +0 -37
  1051. package/src/components/chart/chart-renderer.ts +0 -1562
  1052. package/src/components/chart/chart-resolution.test.ts +0 -87
  1053. package/src/components/chart/chart-resolution.ts +0 -405
  1054. package/src/components/chart/chart-scroll.test.ts +0 -70
  1055. package/src/components/chart/chart-scroll.ts +0 -104
  1056. package/src/components/chart/chart-types.ts +0 -119
  1057. package/src/components/chart/chart-viewport.test.ts +0 -29
  1058. package/src/components/chart/chart-viewport.ts +0 -50
  1059. package/src/components/chart/chart.test.ts +0 -922
  1060. package/src/components/chart/comparison-chart-data.test.ts +0 -105
  1061. package/src/components/chart/comparison-chart-data.ts +0 -292
  1062. package/src/components/chart/comparison-chart-renderer.test.ts +0 -187
  1063. package/src/components/chart/comparison-chart-renderer.ts +0 -437
  1064. package/src/components/chart/native/kitty-adapter.ts +0 -9
  1065. package/src/components/chart/native/kitty-manager.ts +0 -142
  1066. package/src/components/chart/native/kitty-support.ts +0 -71
  1067. package/src/components/chart/native/kitty.test.ts +0 -728
  1068. package/src/components/chart/native/surface-manager.ts +0 -225
  1069. package/src/components/chart/native/surface-sync.ts +0 -34
  1070. package/src/components/chart/native/surface-visibility.ts +0 -48
  1071. package/src/components/chart/static-chart-surface.test.tsx +0 -51
  1072. package/src/components/chart/static-chart-surface.tsx +0 -217
  1073. package/src/components/chart/stock-chart-renderer-switch.test.tsx +0 -1323
  1074. package/src/components/chart/stock-chart.test.ts +0 -330
  1075. package/src/components/chart/stock-chart.tsx +0 -4148
  1076. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -109
  1077. package/src/components/command-bar/command-bar.test.tsx +0 -3244
  1078. package/src/components/command-bar/command-bar.tsx +0 -5886
  1079. package/src/components/command-bar/command-registry.ts +0 -267
  1080. package/src/components/command-bar/confirm-body.tsx +0 -51
  1081. package/src/components/command-bar/list-model.ts +0 -80
  1082. package/src/components/command-bar/pane-template-display.ts +0 -8
  1083. package/src/components/command-bar/root-shortcuts.ts +0 -188
  1084. package/src/components/command-bar/workflow-ops.test.ts +0 -144
  1085. package/src/components/command-bar/workflow-ops.ts +0 -459
  1086. package/src/components/command-bar/workflow-types.ts +0 -127
  1087. package/src/components/data-table-stack-view.test.tsx +0 -241
  1088. package/src/components/data-table-stack-view.tsx +0 -59
  1089. package/src/components/data-table-view.test.tsx +0 -211
  1090. package/src/components/data-table-view.tsx +0 -244
  1091. package/src/components/feed-data-table-stack-view.test.tsx +0 -225
  1092. package/src/components/feed-data-table-stack-view.tsx +0 -359
  1093. package/src/components/layout/pane-content.test.tsx +0 -64
  1094. package/src/components/layout/pane-content.tsx +0 -44
  1095. package/src/components/layout/pane-cycle-order.test.ts +0 -30
  1096. package/src/components/layout/pane-cycle-order.ts +0 -32
  1097. package/src/components/layout/pane-footer.test.tsx +0 -195
  1098. package/src/components/layout/pane-footer.tsx +0 -432
  1099. package/src/components/layout/pane-header.test.tsx +0 -39
  1100. package/src/components/layout/pane-header.tsx +0 -248
  1101. package/src/components/layout/pane-keyboard-scroll.test.tsx +0 -226
  1102. package/src/components/layout/pane-sizing.ts +0 -34
  1103. package/src/components/layout/pane-title.ts +0 -38
  1104. package/src/components/layout/pane.tsx +0 -129
  1105. package/src/components/layout/shell.test.tsx +0 -1453
  1106. package/src/components/layout/shell.tsx +0 -1960
  1107. package/src/components/price-sparkline-view.tsx +0 -463
  1108. package/src/components/price-sparkline.ts +0 -46
  1109. package/src/components/speedometer-gauge.test.tsx +0 -45
  1110. package/src/components/speedometer-gauge.tsx +0 -562
  1111. package/src/components/ticker-badge-list.tsx +0 -53
  1112. package/src/components/ticker-badge-text.test.tsx +0 -253
  1113. package/src/components/ticker-badge-text.tsx +0 -113
  1114. package/src/components/ticker-badge.tsx +0 -109
  1115. package/src/components/ticker-list-table-view.tsx +0 -233
  1116. package/src/components/ticker-list-table.test.tsx +0 -210
  1117. package/src/components/ticker-list-table.tsx +0 -458
  1118. package/src/components/ui/data-table.tsx +0 -522
  1119. package/src/components/ui/multi-select-dialog.tsx +0 -336
  1120. package/src/core/state/app-state.ts +0 -1009
  1121. package/src/data/config-layout.ts +0 -225
  1122. package/src/data/config-store-node.ts +0 -478
  1123. package/src/data/config-store.test.ts +0 -481
  1124. package/src/data/config-store.ts +0 -64
  1125. package/src/data/sqlite-cache.test.ts +0 -151
  1126. package/src/data/sqlite-database.ts +0 -68
  1127. package/src/data/sqlite-json.ts +0 -16
  1128. package/src/data/sqlite-retry.test.ts +0 -63
  1129. package/src/market-data/coordinator-subscriptions.test.ts +0 -209
  1130. package/src/market-data/coordinator.test.ts +0 -830
  1131. package/src/market-data/coordinator.ts +0 -1155
  1132. package/src/plugins/builtin/ai/screener-contract.test.ts +0 -117
  1133. package/src/plugins/builtin/ai/screener-contract.ts +0 -174
  1134. package/src/plugins/builtin/ai/screener-pane.test.tsx +0 -348
  1135. package/src/plugins/builtin/ai/screener-pane.tsx +0 -1118
  1136. package/src/plugins/builtin/ai/utils.ts +0 -1
  1137. package/src/plugins/builtin/ask-ai.test.tsx +0 -348
  1138. package/src/plugins/builtin/chat-controller.test.ts +0 -1376
  1139. package/src/plugins/builtin/chat-controller.ts +0 -1248
  1140. package/src/plugins/builtin/chat.test.tsx +0 -2245
  1141. package/src/plugins/builtin/chat.tsx +0 -2267
  1142. package/src/plugins/builtin/cloud-auth-actions.tsx +0 -72
  1143. package/src/plugins/builtin/cloud-tweets.tsx +0 -896
  1144. package/src/plugins/builtin/comparison-chart.test.tsx +0 -532
  1145. package/src/plugins/builtin/comparison-chart.tsx +0 -194
  1146. package/src/plugins/builtin/debug.tsx +0 -365
  1147. package/src/plugins/builtin/earnings/earnings-cache.test.ts +0 -181
  1148. package/src/plugins/builtin/earnings/earnings-cache.ts +0 -145
  1149. package/src/plugins/builtin/earnings/earnings-data.test.ts +0 -90
  1150. package/src/plugins/builtin/earnings/earnings-data.ts +0 -115
  1151. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +0 -72
  1152. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +0 -366
  1153. package/src/plugins/builtin/help.test.tsx +0 -166
  1154. package/src/plugins/builtin/help.tsx +0 -450
  1155. package/src/plugins/builtin/layout-manager.tsx +0 -247
  1156. package/src/plugins/builtin/manual-entry.tsx +0 -25
  1157. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +0 -98
  1158. package/src/plugins/builtin/news-wire/breaking-notifications.ts +0 -110
  1159. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +0 -156
  1160. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -68
  1161. package/src/plugins/builtin/news-wire/categories.ts +0 -114
  1162. package/src/plugins/builtin/news-wire/default-feeds.ts +0 -3
  1163. package/src/plugins/builtin/news-wire/feed-config.test.ts +0 -90
  1164. package/src/plugins/builtin/news-wire/feed-config.ts +0 -216
  1165. package/src/plugins/builtin/news-wire/index.tsx +0 -129
  1166. package/src/plugins/builtin/news-wire/industry-pane.tsx +0 -126
  1167. package/src/plugins/builtin/news-wire/news-detail-view.tsx +0 -299
  1168. package/src/plugins/builtin/news-wire/news-footer.ts +0 -28
  1169. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +0 -98
  1170. package/src/plugins/builtin/news-wire/news-query-presets.ts +0 -40
  1171. package/src/plugins/builtin/news-wire/news-table.test.tsx +0 -206
  1172. package/src/plugins/builtin/news-wire/news-table.tsx +0 -296
  1173. package/src/plugins/builtin/news-wire/persisted-articles.ts +0 -98
  1174. package/src/plugins/builtin/news-wire/read-state.ts +0 -76
  1175. package/src/plugins/builtin/news-wire/rss-parser.test.ts +0 -184
  1176. package/src/plugins/builtin/news-wire/rss-parser.ts +0 -213
  1177. package/src/plugins/builtin/news-wire/rss-source.test.ts +0 -159
  1178. package/src/plugins/builtin/news-wire/rss-source.ts +0 -181
  1179. package/src/plugins/builtin/news.tsx +0 -194
  1180. package/src/plugins/builtin/notes.tsx +0 -561
  1181. package/src/plugins/builtin/options.test.tsx +0 -241
  1182. package/src/plugins/builtin/options.tsx +0 -542
  1183. package/src/plugins/builtin/portfolio-list/cli/commands.ts +0 -657
  1184. package/src/plugins/builtin/portfolio-list/pane.tsx +0 -724
  1185. package/src/plugins/builtin/portfolio-list/quick-add.tsx +0 -422
  1186. package/src/plugins/builtin/portfolio-list/summary.tsx +0 -275
  1187. package/src/plugins/builtin/portfolio-list.test.ts +0 -133
  1188. package/src/plugins/builtin/portfolio-list.test.tsx +0 -1535
  1189. package/src/plugins/builtin/portfolio-metrics.test.ts +0 -236
  1190. package/src/plugins/builtin/quote-monitor.test.tsx +0 -222
  1191. package/src/plugins/builtin/sec-filing-display.tsx +0 -84
  1192. package/src/plugins/builtin/sec.tsx +0 -342
  1193. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +0 -461
  1194. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +0 -479
  1195. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +0 -269
  1196. package/src/plugins/builtin/ticker-detail.test.tsx +0 -888
  1197. package/src/plugins/builtin/ticker-surface.ts +0 -68
  1198. package/src/plugins/builtin/yahoo.ts +0 -72
  1199. package/src/plugins/catalog-ui.test.ts +0 -24
  1200. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +0 -35
  1201. package/src/plugins/ibkr/flex.test.ts +0 -162
  1202. package/src/plugins/ibkr/flex.ts +0 -371
  1203. package/src/plugins/ibkr/gateway-helpers.ts +0 -33
  1204. package/src/plugins/ibkr/gateway-selection.ts +0 -51
  1205. package/src/plugins/ibkr/gateway-service-native.ts +0 -2377
  1206. package/src/plugins/ibkr/gateway-service.test.ts +0 -584
  1207. package/src/plugins/ibkr/gateway-service.ts +0 -433
  1208. package/src/plugins/ibkr/gateway-types.ts +0 -27
  1209. package/src/plugins/ibkr/price-normalization.test.ts +0 -33
  1210. package/src/plugins/ibkr/price-normalization.ts +0 -60
  1211. package/src/plugins/ibkr/trade-badge.tsx +0 -28
  1212. package/src/plugins/ibkr/trade-tab.test.tsx +0 -276
  1213. package/src/plugins/ibkr/trade-tab.tsx +0 -1117
  1214. package/src/plugins/ibkr/trade-utils.test.ts +0 -175
  1215. package/src/plugins/ibkr/trade-utils.ts +0 -150
  1216. package/src/plugins/ibkr/trading-pane.tsx +0 -474
  1217. package/src/plugins/ibkr/trading-state.test.ts +0 -109
  1218. package/src/plugins/ibkr/trading-state.ts +0 -339
  1219. package/src/plugins/plugin-runtime.test.tsx +0 -323
  1220. package/src/plugins/plugin-runtime.tsx +0 -433
  1221. package/src/plugins/prediction-markets/controller-data.ts +0 -785
  1222. package/src/plugins/prediction-markets/controller.ts +0 -659
  1223. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +0 -572
  1224. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +0 -696
  1225. package/src/plugins/prediction-markets/services/polymarket-ws.ts +0 -128
  1226. package/src/plugins/registry.test.ts +0 -362
  1227. package/src/plugins/registry.ts +0 -854
  1228. package/src/renderers/electrobun/bun/application-menu-click.test.ts +0 -50
  1229. package/src/renderers/electrobun/bun/application-menu-click.ts +0 -58
  1230. package/src/renderers/electrobun/bun/application-menu.ts +0 -123
  1231. package/src/renderers/electrobun/bun/context-menu-click.test.ts +0 -67
  1232. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +0 -52
  1233. package/src/renderers/electrobun/bun/context-menu-normalize.ts +0 -67
  1234. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +0 -63
  1235. package/src/renderers/electrobun/bun/desktop-workspace.ts +0 -180
  1236. package/src/renderers/electrobun/bun/window-focus.ts +0 -37
  1237. package/src/renderers/electrobun/bun/window-frame.test.ts +0 -40
  1238. package/src/renderers/electrobun/view/data-table.tsx +0 -810
  1239. package/src/renderers/electrobun/view/desktop-controls.tsx +0 -673
  1240. package/src/renderers/electrobun/view/desktop-window-bridge.ts +0 -70
  1241. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +0 -57
  1242. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +0 -41
  1243. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +0 -9
  1244. package/src/renderers/opentui/image-loader.test.ts +0 -25
  1245. package/src/renderers/opentui/image-loader.ts +0 -105
  1246. package/src/renderers/opentui/image-surface.tsx +0 -266
  1247. package/src/sources/gloomberb-cloud.test.ts +0 -637
  1248. package/src/sources/gloomberb-cloud.ts +0 -828
  1249. package/src/sources/provider-router.test.ts +0 -2600
  1250. package/src/sources/provider-router.ts +0 -2231
  1251. package/src/state/app-activity.test.ts +0 -41
  1252. package/src/state/app-activity.ts +0 -106
  1253. package/src/state/app-bootstrap.test.ts +0 -498
  1254. package/src/state/app-bootstrap.ts +0 -411
  1255. package/src/state/app-context-selector.test.tsx +0 -369
  1256. package/src/state/app-context.test.ts +0 -670
  1257. package/src/state/app-context.test.tsx +0 -193
  1258. package/src/state/app-context.tsx +0 -485
  1259. package/src/state/use-inline-tickers.test.tsx +0 -73
  1260. package/src/state/use-inline-tickers.ts +0 -188
  1261. package/src/state/use-quote-streaming.test.ts +0 -31
  1262. package/src/state/use-quote-streaming.test.tsx +0 -70
  1263. package/src/state/use-quote-streaming.ts +0 -92
  1264. package/src/state/use-session-snapshot.ts +0 -99
  1265. package/src/utils/api-client.test.ts +0 -603
  1266. package/src/utils/api-client.ts +0 -1379
  1267. package/src/utils/market-format.test.ts +0 -109
  1268. package/src/utils/market-format.ts +0 -299
  1269. package/src/utils/market-status.ts +0 -141
  1270. package/src/utils/quote-freshness.test.ts +0 -150
  1271. package/src/utils/quote-freshness.ts +0 -145
  1272. package/src/utils/quote-resolution.test.ts +0 -277
  1273. package/src/utils/quote-resolution.ts +0 -642
  1274. package/src/utils/quote-time.test.ts +0 -34
  1275. package/src/utils/quote-time.ts +0 -59
  1276. package/src/utils/ticker-list.test.ts +0 -16
  1277. package/src/utils/ticker-search.test.ts +0 -227
  1278. package/src/utils/ticker-search.ts +0 -926
  1279. package/src/utils/ticker-tokenizer.test.ts +0 -31
  1280. package/src/utils/ticker-tokenizer.ts +0 -37
  1281. /package/src/components/chart/native/{kitty-protocol.ts → kitty/protocol.ts} +0 -0
  1282. /package/src/components/ui/{multi-select.ts → multi-select/index.ts} +0 -0
  1283. /package/src/data/{sqlite-retry.ts → sqlite/retry.ts} +0 -0
  1284. /package/src/plugins/builtin/{news-wire → news/wire}/hash.ts +0 -0
  1285. /package/src/plugins/builtin/{news-wire → news/wire}/read-state.test.ts +0 -0
  1286. /package/src/plugins/builtin/{notes-files.ts → notes/files.ts} +0 -0
  1287. /package/src/renderers/electrobun/bun/{context-menu-click.ts → context-menu/click.ts} +0 -0
  1288. /package/src/renderers/electrobun/bun/{window-frame.ts → window/frame.ts} +0 -0
  1289. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-manager.ts → chart/surface-manager.ts} +0 -0
  1290. /package/src/renderers/electrobun/view/native-stubs/{chart-surface-sync.ts → chart/surface-sync.ts} +0 -0
  1291. /package/src/{utils/ticker-list.ts → tickers/list.ts} +0 -0
  1292. /package/src/{utils/ticker-metadata.ts → tickers/metadata.ts} +0 -0
@@ -1,2245 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { act, useCallback, useMemo, useRef, useState } from "react";
3
- import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
4
- import { testRender } from "../../renderers/opentui/test-utils";
5
- import { AppContext, appReducer, createInitialState, PaneInstanceProvider } from "../../state/app-context";
6
- import { createConfigBackedTestPluginRuntime, createTestPluginRuntime } from "../../test-support/plugin-runtime";
7
- import { colors } from "../../theme/colors";
8
- import { createDefaultConfig, findPaneInstance, type PaneInstanceConfig } from "../../types/config";
9
- import type { PersistedResourceValue } from "../../types/persistence";
10
- import type { PluginPersistence } from "../../types/plugin";
11
- import { Box, TextAttributes } from "../../ui";
12
- import { apiClient, type ChatChannel, type ChatMessage } from "../../utils/api-client";
13
- import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
14
- import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
15
- import { ChatContent, ChatStatusWidget, getScrollTopForElementIntoView, getSelectedMessageScrollTop, gloomberbCloudPlugin } from "./chat";
16
- import { chatController, ChatController } from "./chat-controller";
17
-
18
- const TRANSCRIPT_KIND = "channel-transcript";
19
- const TRANSCRIPT_KEY = "everyone";
20
- const TRANSCRIPT_SOURCE = "server";
21
- const TRANSCRIPT_SCHEMA_VERSION = 2;
22
- const originalConnectChannel = apiClient.connectChannel.bind(apiClient);
23
- const originalGetChannels = apiClient.getChannels.bind(apiClient);
24
- const originalGetChatPresence = apiClient.getChatPresence.bind(apiClient);
25
- const originalUpdateChatChannelState = apiClient.updateChatChannelState.bind(apiClient);
26
-
27
- let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
28
-
29
- const TEST_CHAT_CHANNELS: ChatChannel[] = [
30
- { id: "everyone", name: "everyone", created_at: "2026-03-26T12:10:05.684Z" },
31
- { id: "equities", name: "equities", created_at: "2026-05-09T00:00:00.000Z" },
32
- { id: "options", name: "options", created_at: "2026-05-09T00:00:00.000Z" },
33
- { id: "macro", name: "macro", created_at: "2026-05-09T00:00:00.000Z" },
34
- { id: "crypto", name: "crypto", created_at: "2026-05-09T00:00:00.000Z" },
35
- { id: "energy", name: "energy", created_at: "2026-05-09T00:00:00.000Z" },
36
- { id: "help", name: "help", created_at: "2026-05-09T00:00:00.000Z" },
37
- ];
38
-
39
- function installServerChannels(controller: ChatController, channels = TEST_CHAT_CHANNELS): void {
40
- (controller as any).channels = channels;
41
- }
42
-
43
- class MemoryPersistence implements PluginPersistence {
44
- private readonly state = new Map<string, { schemaVersion: number; value: unknown }>();
45
- private readonly resources = new Map<string, PersistedResourceValue<unknown>>();
46
-
47
- getState<T = unknown>(key: string, options?: { schemaVersion?: number }): T | null {
48
- const record = this.state.get(key);
49
- if (!record) return null;
50
- if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) {
51
- this.state.delete(key);
52
- return null;
53
- }
54
- return record.value as T;
55
- }
56
-
57
- setState(key: string, value: unknown, options?: { schemaVersion?: number }): void {
58
- this.state.set(key, { schemaVersion: options?.schemaVersion ?? 1, value });
59
- }
60
-
61
- deleteState(key: string): void {
62
- this.state.delete(key);
63
- }
64
-
65
- getResource<T = unknown>(
66
- kind: string,
67
- key: string,
68
- options?: { sourceKey?: string; schemaVersion?: number; allowExpired?: boolean },
69
- ): PersistedResourceValue<T> | null {
70
- const record = this.resources.get(`${kind}:${key}:${options?.sourceKey ?? ""}`);
71
- if (!record) return null;
72
- if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) {
73
- this.resources.delete(`${kind}:${key}:${options.sourceKey ?? ""}`);
74
- return null;
75
- }
76
- return record as PersistedResourceValue<T>;
77
- }
78
-
79
- setResource<T = unknown>(
80
- kind: string,
81
- key: string,
82
- value: T,
83
- options: {
84
- cachePolicy: { staleMs: number; expireMs: number };
85
- sourceKey?: string;
86
- schemaVersion?: number;
87
- provenance?: unknown;
88
- },
89
- ): PersistedResourceValue<T> {
90
- const now = Date.now();
91
- const record: PersistedResourceValue<T> = {
92
- value,
93
- fetchedAt: now,
94
- staleAt: now + options.cachePolicy.staleMs,
95
- expiresAt: now + options.cachePolicy.expireMs,
96
- sourceKey: options.sourceKey ?? "",
97
- schemaVersion: options.schemaVersion ?? 1,
98
- provenance: options.provenance,
99
- };
100
- this.resources.set(`${kind}:${key}:${options.sourceKey ?? ""}`, record);
101
- return record;
102
- }
103
-
104
- deleteResource(kind: string, key: string, options?: { sourceKey?: string }): void {
105
- this.resources.delete(`${kind}:${key}:${options?.sourceKey ?? ""}`);
106
- }
107
- }
108
-
109
- function makeMessage(index: number): ChatMessage {
110
- return {
111
- id: `m${index}`,
112
- channelId: "everyone",
113
- content: `message ${index}`,
114
- replyToId: null,
115
- createdAt: `2026-03-30T00:00:${String(index).padStart(2, "0")}.000Z`,
116
- user: {
117
- id: `u${index}`,
118
- username: `user${index}`,
119
- displayName: `User ${index}`,
120
- },
121
- };
122
- }
123
-
124
- function createController(options: {
125
- messages?: ChatMessage[];
126
- sessionToken?: string | null;
127
- user?: { id: string; username: string; emailVerified: boolean } | null;
128
- replyToId?: string | null;
129
- } = {}) {
130
- const messages = options.messages ?? [];
131
- const persistence = new MemoryPersistence();
132
- const controller = new ChatController();
133
- const user = Object.prototype.hasOwnProperty.call(options, "user")
134
- ? options.user ?? null
135
- : { id: "u0", username: "vince", emailVerified: true };
136
- persistence.setState("session", {
137
- sessionToken: options.sessionToken ?? null,
138
- user,
139
- }, { schemaVersion: 1 });
140
- persistence.setState("channel:everyone", {
141
- draft: "",
142
- replyToId: options.replyToId ?? null,
143
- lastCursor: messages[messages.length - 1]?.id ?? null,
144
- }, { schemaVersion: 1 });
145
- if (messages.length > 0) {
146
- persistence.setResource(TRANSCRIPT_KIND, TRANSCRIPT_KEY, { messages }, {
147
- sourceKey: TRANSCRIPT_SOURCE,
148
- schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
149
- cachePolicy: { staleMs: 1_000, expireMs: 2_000 },
150
- });
151
- }
152
- controller.attachPersistence(persistence);
153
- controller.refreshSession = async () => {};
154
- controller.refreshMessages = async () => {};
155
- controller.refreshPresence = async () => {};
156
- controller.refreshChatState = async () => {};
157
- return controller;
158
- }
159
-
160
- function createHarness(
161
- controller: ChatController,
162
- options?: {
163
- width?: number;
164
- height?: number;
165
- configureState?: (state: ReturnType<typeof createInitialState>) => void;
166
- close?: () => void;
167
- withFooter?: boolean;
168
- runtime?: PluginRuntimeAccess;
169
- },
170
- ) {
171
- const width = options?.width ?? 60;
172
- const height = options?.height ?? 12;
173
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
174
- options?.configureState?.(state);
175
-
176
- const content = options?.withFooter ? (
177
- <PaneFooterProvider>
178
- {(footer) => (
179
- <Box flexDirection="column" width={width} height={height}>
180
- <ChatContent
181
- controller={controller}
182
- width={width}
183
- height={Math.max(1, height - 1)}
184
- focused
185
- close={options?.close}
186
- />
187
- <PaneFooterBar footer={footer} focused width={width} />
188
- </Box>
189
- )}
190
- </PaneFooterProvider>
191
- ) : (
192
- <ChatContent
193
- controller={controller}
194
- width={width}
195
- height={height}
196
- focused
197
- close={options?.close}
198
- />
199
- );
200
-
201
- return (
202
- <AppContext value={{ state, dispatch: () => {} }}>
203
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={options?.runtime ?? createTestPluginRuntime()}>
204
- {content}
205
- </PluginRenderProvider>
206
- </AppContext>
207
- );
208
- }
209
-
210
- async function flushFrame() {
211
- await act(async () => {
212
- await testSetup!.renderOnce();
213
- await testSetup!.renderOnce();
214
- });
215
- }
216
-
217
- async function emitKeypress(event: {
218
- name?: string;
219
- sequence?: string;
220
- ctrl?: boolean;
221
- meta?: boolean;
222
- shift?: boolean;
223
- option?: boolean;
224
- }) {
225
- await act(async () => {
226
- testSetup!.renderer.keyInput.emit("keypress", {
227
- ctrl: false,
228
- meta: false,
229
- option: false,
230
- shift: false,
231
- eventType: "press",
232
- repeated: false,
233
- stopPropagation: () => {},
234
- preventDefault: () => {},
235
- ...event,
236
- } as any);
237
- await testSetup!.renderOnce();
238
- });
239
- }
240
-
241
- function lineText(line: { spans: Array<{ text: string }> }) {
242
- return line.spans.map((span) => span.text).join("");
243
- }
244
-
245
- function hexToRgbaInts(hex: string) {
246
- const normalized = hex.replace("#", "");
247
- return [
248
- parseInt(normalized.slice(0, 2), 16),
249
- parseInt(normalized.slice(2, 4), 16),
250
- parseInt(normalized.slice(4, 6), 16),
251
- 255,
252
- ].join(",");
253
- }
254
-
255
- beforeEach(() => {
256
- apiClient.getChatPresence = async () => ({ onlineCount: 0 });
257
- apiClient.updateChatChannelState = async (channelId, body) => ({
258
- channelId,
259
- notificationsEnabled: body.notificationsEnabled ?? false,
260
- lastReadMessageId: body.readThroughMessageId ?? null,
261
- unreadCount: 0,
262
- });
263
- });
264
-
265
- afterEach(async () => {
266
- setSharedRegistryForTests(undefined);
267
- setSharedMarketDataForTests(undefined);
268
- apiClient.connectChannel = originalConnectChannel;
269
- apiClient.getChannels = originalGetChannels;
270
- apiClient.getChatPresence = originalGetChatPresence;
271
- apiClient.updateChatChannelState = originalUpdateChatChannelState;
272
- apiClient.setSessionToken(null);
273
-
274
- if (testSetup) {
275
- await act(async () => {
276
- testSetup!.renderer.destroy();
277
- });
278
- testSetup = undefined;
279
- }
280
- });
281
-
282
- describe("ChatContent", () => {
283
- test("focuses the prompt on click and preserves typing order", async () => {
284
- const controller = createController();
285
-
286
- await act(async () => {
287
- testSetup = await testRender(createHarness(controller), {
288
- width: 60,
289
- height: 12,
290
- });
291
- });
292
-
293
- await flushFrame();
294
-
295
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
296
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
297
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
298
-
299
- expect(inputRow).toBeGreaterThanOrEqual(0);
300
- expect(inputCol).toBeGreaterThanOrEqual(0);
301
-
302
- await act(async () => {
303
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
304
- await testSetup!.renderOnce();
305
- await testSetup!.renderOnce();
306
- });
307
-
308
- await act(async () => {
309
- await testSetup!.mockInput.typeText("DCF");
310
- await testSetup!.renderOnce();
311
- await testSetup!.renderOnce();
312
- });
313
-
314
- const frameAfterType = testSetup.captureCharFrame();
315
- expect(frameAfterType).toContain("> DCF");
316
- expect(frameAfterType).not.toContain("> FCD");
317
- });
318
-
319
- test("keeps appending typed text while transcript updates arrive", async () => {
320
- const controller = createController({
321
- messages: [makeMessage(1)],
322
- });
323
-
324
- await act(async () => {
325
- testSetup = await testRender(createHarness(controller), {
326
- width: 60,
327
- height: 12,
328
- });
329
- });
330
-
331
- await flushFrame();
332
-
333
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
334
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
335
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
336
-
337
- expect(inputRow).toBeGreaterThanOrEqual(0);
338
- expect(inputCol).toBeGreaterThanOrEqual(0);
339
-
340
- await act(async () => {
341
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
342
- await testSetup!.renderOnce();
343
- await testSetup!.renderOnce();
344
- });
345
-
346
- await act(async () => {
347
- await testSetup!.mockInput.typeText("alpha");
348
- await testSetup!.renderOnce();
349
- await testSetup!.renderOnce();
350
- });
351
-
352
- await act(async () => {
353
- (controller as any).mergeMessages([makeMessage(2)]);
354
- });
355
-
356
- await flushFrame();
357
-
358
- await act(async () => {
359
- await testSetup!.mockInput.typeText("beta");
360
- await testSetup!.renderOnce();
361
- await testSetup!.renderOnce();
362
- });
363
-
364
- const frameAfterType = testSetup.captureCharFrame();
365
- expect(frameAfterType).toContain("> alphabeta");
366
- expect(frameAfterType).not.toContain("> betaalpha");
367
- });
368
-
369
- test("does not render an idle helper row above the composer", async () => {
370
- const controller = createController({
371
- messages: [makeMessage(1)],
372
- });
373
-
374
- await act(async () => {
375
- testSetup = await testRender(createHarness(controller, {
376
- width: 72,
377
- height: 12,
378
- }), {
379
- width: 72,
380
- height: 12,
381
- });
382
- });
383
-
384
- await flushFrame();
385
-
386
- const frame = testSetup.captureCharFrame();
387
- expect(frame).toContain("Type a message...");
388
- expect(frame).not.toContain("click a message");
389
- });
390
-
391
- test("shows the channel sidebar on wide panes and hides it on narrow panes", async () => {
392
- const controller = createController({ sessionToken: "token-123" });
393
- installServerChannels(controller);
394
- controller.refreshChannels = async () => {};
395
- controller.refreshChannelMessages = async () => {};
396
-
397
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
398
- const renderChannelPane = (width: number) => (
399
- <AppContext value={{ state, dispatch: () => {} }}>
400
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
401
- <ChatContent
402
- controller={controller}
403
- width={width}
404
- height={12}
405
- focused
406
- channelId="options"
407
- onChannelChange={() => {}}
408
- />
409
- </PluginRenderProvider>
410
- </AppContext>
411
- );
412
-
413
- await act(async () => {
414
- testSetup = await testRender(renderChannelPane(90), {
415
- width: 90,
416
- height: 12,
417
- });
418
- });
419
-
420
- await flushFrame();
421
- expect(testSetup.captureCharFrame()).toContain("options");
422
-
423
- await act(async () => {
424
- testSetup?.renderer.destroy();
425
- testSetup = await testRender(renderChannelPane(60), {
426
- width: 60,
427
- height: 12,
428
- });
429
- });
430
-
431
- await flushFrame();
432
- expect(testSetup.captureCharFrame()).not.toContain("options");
433
- });
434
-
435
- test("selects a sidebar channel from a single text click", async () => {
436
- const controller = createController({ sessionToken: "token-123" });
437
- installServerChannels(controller);
438
- controller.refreshChannels = async () => {};
439
- controller.refreshChannelMessages = async () => {};
440
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
441
-
442
- function ChannelPane() {
443
- const [channelId, setChannelId] = useState("equities");
444
- return (
445
- <AppContext value={{ state, dispatch: () => {} }}>
446
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
447
- <ChatContent
448
- controller={controller}
449
- width={90}
450
- height={12}
451
- focused
452
- channelId={channelId}
453
- onChannelChange={setChannelId}
454
- />
455
- </PluginRenderProvider>
456
- </AppContext>
457
- );
458
- }
459
-
460
- await act(async () => {
461
- testSetup = await testRender(<ChannelPane />, {
462
- width: 90,
463
- height: 12,
464
- });
465
- });
466
-
467
- await flushFrame();
468
- expect(testSetup.captureCharFrame()).toContain("#equities");
469
-
470
- const lines = testSetup.captureCharFrame().split("\n");
471
- const row = lines.findIndex((line) => line.includes("options"));
472
- const col = lines[row]?.indexOf("options") ?? -1;
473
-
474
- expect(row).toBeGreaterThanOrEqual(0);
475
- expect(col).toBeGreaterThanOrEqual(0);
476
-
477
- await act(async () => {
478
- await testSetup!.mockMouse.click(col + 1, row);
479
- await testSetup!.renderOnce();
480
- await testSetup!.renderOnce();
481
- });
482
- await flushFrame();
483
-
484
- expect(testSetup.captureCharFrame()).toContain("#options");
485
- });
486
-
487
- test("renders unread sidebar channels in bold and clears them when opened", async () => {
488
- const controller = createController({ sessionToken: "token-123" });
489
- installServerChannels(controller);
490
- controller.refreshChannels = async () => {};
491
- controller.refreshChannelMessages = async () => {};
492
- const optionsState = (controller as any).ensureChannelState("options");
493
- optionsState.unreadCount = 2;
494
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
495
-
496
- function ChannelPane() {
497
- const [channelId, setChannelId] = useState("equities");
498
- return (
499
- <AppContext value={{ state, dispatch: () => {} }}>
500
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
501
- <ChatContent
502
- controller={controller}
503
- width={90}
504
- height={12}
505
- focused
506
- channelId={channelId}
507
- onChannelChange={setChannelId}
508
- />
509
- </PluginRenderProvider>
510
- </AppContext>
511
- );
512
- }
513
-
514
- await act(async () => {
515
- testSetup = await testRender(<ChannelPane />, {
516
- width: 90,
517
- height: 12,
518
- });
519
- });
520
-
521
- await flushFrame();
522
- const unreadLine = testSetup.captureSpans().lines.find((line) => lineText(line).includes("options"));
523
- const unreadSpan = unreadLine?.spans.find((span) => span.text.includes("options"));
524
- expect((unreadSpan?.attributes ?? 0) & TextAttributes.BOLD).toBe(TextAttributes.BOLD);
525
-
526
- const lines = testSetup.captureCharFrame().split("\n");
527
- const row = lines.findIndex((line) => line.includes("options"));
528
- const col = lines[row]?.indexOf("options") ?? -1;
529
-
530
- await act(async () => {
531
- await testSetup!.mockMouse.click(col + 1, row);
532
- await testSetup!.renderOnce();
533
- await testSetup!.renderOnce();
534
- });
535
- await flushFrame();
536
-
537
- const readLine = testSetup.captureSpans().lines.find((line) => lineText(line).includes("#options"));
538
- const readSpan = readLine?.spans.find((span) => span.text.includes("options"));
539
- expect((readSpan?.attributes ?? 0) & TextAttributes.BOLD).toBe(0);
540
- });
541
-
542
- test("toggles sidebar channel notifications without selecting the channel", async () => {
543
- const controller = createController({ sessionToken: "token-123" });
544
- installServerChannels(controller);
545
- controller.refreshChannels = async () => {};
546
- controller.refreshChannelMessages = async () => {};
547
- const toggles: Array<{ channelId: string; enabled: boolean }> = [];
548
- controller.setChannelNotificationsEnabled = (channelId: string, enabled: boolean) => {
549
- toggles.push({ channelId, enabled });
550
- };
551
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
552
-
553
- function ChannelPane() {
554
- const [channelId, setChannelId] = useState("equities");
555
- return (
556
- <AppContext value={{ state, dispatch: () => {} }}>
557
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
558
- <ChatContent
559
- controller={controller}
560
- width={90}
561
- height={12}
562
- focused
563
- channelId={channelId}
564
- onChannelChange={setChannelId}
565
- />
566
- </PluginRenderProvider>
567
- </AppContext>
568
- );
569
- }
570
-
571
- await act(async () => {
572
- testSetup = await testRender(<ChannelPane />, {
573
- width: 90,
574
- height: 12,
575
- });
576
- });
577
-
578
- await flushFrame();
579
- const frame = testSetup.captureCharFrame();
580
- const lines = frame.split("\n");
581
- const row = lines.findIndex((line) => line.includes("options"));
582
- const col = lines[row]?.lastIndexOf("·") ?? -1;
583
- expect(row).toBeGreaterThanOrEqual(0);
584
- expect(col).toBeGreaterThanOrEqual(0);
585
-
586
- await act(async () => {
587
- await testSetup!.mockMouse.click(col, row);
588
- await testSetup!.renderOnce();
589
- await testSetup!.renderOnce();
590
- });
591
- await flushFrame();
592
-
593
- expect(toggles).toEqual([{ channelId: "options", enabled: true }]);
594
- expect(testSetup.captureCharFrame()).toContain("#equities");
595
- expect(testSetup.captureCharFrame()).not.toContain("#options");
596
- });
597
-
598
- test("shows the sidebar online count footer", async () => {
599
- const controller = createController({ sessionToken: "token-123" });
600
- installServerChannels(controller);
601
- controller.refreshChannels = async () => {};
602
- controller.refreshChannelMessages = async () => {};
603
- (controller as any).onlineCount = 6;
604
-
605
- await act(async () => {
606
- testSetup = await testRender(createHarness(controller, { width: 90, height: 12 }), {
607
- width: 90,
608
- height: 12,
609
- });
610
- });
611
-
612
- await flushFrame();
613
-
614
- expect(testSetup.captureCharFrame()).toContain("● 6 online");
615
- });
616
-
617
- test("uses arrows to move between channel sidebar and chat content", async () => {
618
- const controller = createController({ sessionToken: "token-123" });
619
- installServerChannels(controller);
620
- controller.refreshChannels = async () => {};
621
- controller.refreshChannelMessages = async () => {};
622
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
623
-
624
- function ChannelPane() {
625
- const [channelId, setChannelId] = useState("equities");
626
- return (
627
- <AppContext value={{ state, dispatch: () => {} }}>
628
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
629
- <ChatContent
630
- controller={controller}
631
- width={90}
632
- height={12}
633
- focused
634
- channelId={channelId}
635
- onChannelChange={setChannelId}
636
- />
637
- </PluginRenderProvider>
638
- </AppContext>
639
- );
640
- }
641
-
642
- await act(async () => {
643
- testSetup = await testRender(<ChannelPane />, {
644
- width: 90,
645
- height: 12,
646
- });
647
- });
648
-
649
- await flushFrame();
650
- expect(testSetup.captureCharFrame()).toContain("#equities");
651
- const getActiveChannelBackgrounds = () => {
652
- const activeLine = testSetup!.captureSpans().lines.find((line) => lineText(line).includes("#equities"));
653
- expect(activeLine).toBeDefined();
654
- return activeLine!.spans.map((span) => span.bg.toInts().join(","));
655
- };
656
- const getContentBackgrounds = () => {
657
- const contentLine = testSetup!.captureSpans().lines.find((line) => lineText(line).includes("No messages yet"));
658
- expect(contentLine).toBeDefined();
659
- return contentLine!.spans.map((span) => span.bg.toInts().join(","));
660
- };
661
- const contentFocusedBackgrounds = getActiveChannelBackgrounds();
662
- const rightFocusedBackgrounds = getContentBackgrounds();
663
-
664
- await emitKeypress({ name: "down", sequence: "\u001b[B" });
665
- expect(testSetup.captureCharFrame()).toContain("#equities");
666
-
667
- await emitKeypress({ name: "left", sequence: "\u001b[D" });
668
- await flushFrame();
669
- expect(getActiveChannelBackgrounds()).not.toEqual(contentFocusedBackgrounds);
670
- expect(getContentBackgrounds()).not.toEqual(rightFocusedBackgrounds);
671
- await emitKeypress({ name: "down", sequence: "\u001b[B" });
672
- await flushFrame();
673
- expect(testSetup.captureCharFrame()).toContain("#options");
674
-
675
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
676
- await flushFrame();
677
- expect(testSetup.captureCharFrame()).toContain("#equities");
678
-
679
- await emitKeypress({ name: "right", sequence: "\u001b[C" });
680
- await flushFrame();
681
- expect(getActiveChannelBackgrounds()).toEqual(contentFocusedBackgrounds);
682
- expect(getContentBackgrounds()).toEqual(rightFocusedBackgrounds);
683
- await emitKeypress({ name: "down", sequence: "\u001b[B" });
684
- await flushFrame();
685
- expect(testSetup.captureCharFrame()).toContain("#equities");
686
- });
687
-
688
- test("keeps the channel sidebar visible while a channel loads", async () => {
689
- const controller = createController({ sessionToken: "token-123" });
690
- installServerChannels(controller);
691
- const getSnapshot = controller.getSnapshot.bind(controller);
692
- controller.getSnapshot = ((channelId?: string) => ({
693
- ...getSnapshot(channelId),
694
- loading: true,
695
- messages: [],
696
- })) as typeof controller.getSnapshot;
697
- controller.refreshChannels = async () => {};
698
- controller.refreshChannelMessages = async () => {};
699
-
700
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
701
-
702
- await act(async () => {
703
- testSetup = await testRender(
704
- <AppContext value={{ state, dispatch: () => {} }}>
705
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
706
- <ChatContent
707
- controller={controller}
708
- width={90}
709
- height={12}
710
- focused
711
- channelId="options"
712
- onChannelChange={() => {}}
713
- />
714
- </PluginRenderProvider>
715
- </AppContext>,
716
- {
717
- width: 90,
718
- height: 12,
719
- },
720
- );
721
- });
722
-
723
- await flushFrame();
724
-
725
- const frame = testSetup.captureCharFrame();
726
- expect(frame).toContain("#options");
727
- expect(frame).toContain("Loading...");
728
- });
729
-
730
- test("persists a pane channel selection without the last-visited write reverting it", async () => {
731
- const registeredPanes: Array<{ id: string; component: (props: any) => JSX.Element }> = [];
732
- gloomberbCloudPlugin.setup({
733
- persistence: new MemoryPersistence(),
734
- resume: {
735
- getState: () => null,
736
- setState: () => {},
737
- deleteState: () => {},
738
- },
739
- registerPane: (pane: { id: string; component: (props: any) => JSX.Element }) => {
740
- registeredPanes.push(pane);
741
- },
742
- registerPaneTemplate: () => {},
743
- registerDetailTab: () => {},
744
- registerShortcut: () => {},
745
- registerCommand: () => {},
746
- showPane: () => {},
747
- hidePane: () => {},
748
- notify: () => {},
749
- } as any);
750
- const ChatPaneComponent = registeredPanes.find((pane) => pane.id === "chat")?.component;
751
- expect(ChatPaneComponent).toBeDefined();
752
- const ResolvedChatPaneComponent = ChatPaneComponent!;
753
-
754
- const originalRefreshChannels = chatController.refreshChannels;
755
- const originalRefreshSession = chatController.refreshSession;
756
- const originalRefreshChannelMessages = chatController.refreshChannelMessages;
757
- installServerChannels(chatController);
758
- chatController.refreshChannels = async () => {};
759
- chatController.refreshSession = async () => {};
760
- chatController.refreshChannelMessages = async () => {};
761
-
762
- const paneInstanceId = "chat:test";
763
- const chatInstance: PaneInstanceConfig = {
764
- instanceId: paneInstanceId,
765
- paneId: "chat",
766
- settings: { channelId: "equities" },
767
- };
768
- const layout = {
769
- dockRoot: { kind: "pane" as const, instanceId: paneInstanceId },
770
- floating: [],
771
- detached: [],
772
- instances: [chatInstance],
773
- };
774
- const initialState = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
775
- initialState.focusedPaneId = paneInstanceId;
776
- initialState.config = {
777
- ...initialState.config,
778
- layout,
779
- layouts: [{ name: "Test", layout }],
780
- pluginConfig: {
781
- "gloomberb-cloud": {
782
- lastChatChannelId: "equities",
783
- },
784
- },
785
- };
786
- let latestState = initialState;
787
-
788
- function ChatPaneHarness() {
789
- const [state, setState] = useState(initialState);
790
- const stateRef = useRef(state);
791
- stateRef.current = state;
792
- latestState = state;
793
- const dispatch = useCallback((action: any) => {
794
- setState((current) => {
795
- const next = appReducer(current, action);
796
- latestState = next;
797
- return next;
798
- });
799
- }, []);
800
- const runtime = useMemo(() => createConfigBackedTestPluginRuntime({
801
- getConfig: () => stateRef.current.config,
802
- setConfig: (config) => dispatch({ type: "SET_CONFIG", config }),
803
- }), [dispatch]);
804
-
805
- return (
806
- <AppContext value={{ state, dispatch }}>
807
- <PaneInstanceProvider paneId={paneInstanceId}>
808
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={runtime}>
809
- <ResolvedChatPaneComponent
810
- width={90}
811
- height={12}
812
- focused
813
- close={() => {}}
814
- />
815
- </PluginRenderProvider>
816
- </PaneInstanceProvider>
817
- </AppContext>
818
- );
819
- }
820
-
821
- try {
822
- await act(async () => {
823
- testSetup = await testRender(<ChatPaneHarness />, {
824
- width: 90,
825
- height: 12,
826
- });
827
- });
828
-
829
- await flushFrame();
830
- const lines = testSetup.captureCharFrame().split("\n");
831
- const row = lines.findIndex((line) => line.includes("options"));
832
- const col = lines[row]?.indexOf("options") ?? -1;
833
-
834
- expect(row).toBeGreaterThanOrEqual(0);
835
- expect(col).toBeGreaterThanOrEqual(0);
836
-
837
- await act(async () => {
838
- await testSetup!.mockMouse.click(col + 1, row);
839
- await testSetup!.renderOnce();
840
- await testSetup!.renderOnce();
841
- });
842
- await flushFrame();
843
-
844
- expect(findPaneInstance(latestState.config.layout, paneInstanceId)?.settings?.channelId).toBe("options");
845
- expect(latestState.config.pluginConfig["gloomberb-cloud"]?.lastChatChannelId).toBe("options");
846
- expect(testSetup.captureCharFrame()).toContain("#options");
847
- } finally {
848
- chatController.refreshChannels = originalRefreshChannels;
849
- chatController.refreshSession = originalRefreshSession;
850
- chatController.refreshChannelMessages = originalRefreshChannelMessages;
851
- installServerChannels(chatController, []);
852
- chatController.dispose();
853
- }
854
- });
855
-
856
- test("uses the full message row width when the reply action is hidden", async () => {
857
- const controller = createController({
858
- messages: [{
859
- ...makeMessage(1),
860
- content: "small panes should use full width",
861
- }],
862
- });
863
-
864
- await act(async () => {
865
- testSetup = await testRender(createHarness(controller, {
866
- width: 44,
867
- height: 8,
868
- }), {
869
- width: 44,
870
- height: 8,
871
- });
872
- });
873
-
874
- await flushFrame();
875
-
876
- expect(testSetup.captureCharFrame()).toContain("small panes should use full width");
877
- });
878
-
879
- test("does not select a message when the row is clicked", async () => {
880
- const controller = createController({
881
- messages: [makeMessage(1)],
882
- });
883
-
884
- await act(async () => {
885
- testSetup = await testRender(createHarness(controller, {
886
- width: 72,
887
- height: 12,
888
- }), {
889
- width: 72,
890
- height: 12,
891
- });
892
- });
893
-
894
- await flushFrame();
895
-
896
- const lines = testSetup.captureCharFrame().split("\n");
897
- const messageRow = lines.findIndex((line) => line.includes("message 1"));
898
- const messageCol = lines[messageRow]?.indexOf("message 1") ?? -1;
899
-
900
- expect(messageRow).toBeGreaterThanOrEqual(0);
901
- expect(messageCol).toBeGreaterThanOrEqual(0);
902
-
903
- await act(async () => {
904
- await testSetup!.mockMouse.click(messageCol + 1, messageRow);
905
- await testSetup!.renderOnce();
906
- await testSetup!.renderOnce();
907
- });
908
-
909
- const frame = testSetup.captureCharFrame();
910
- expect(frame).toContain("Type a message...");
911
- expect(frame).not.toContain("Reply to @user1...");
912
- expect(frame).not.toContain("Cancel");
913
- });
914
-
915
- test("down arrow does not jump to the oldest message when nothing is selected", async () => {
916
- const controller = createController({
917
- messages: [makeMessage(1)],
918
- });
919
-
920
- await act(async () => {
921
- testSetup = await testRender(createHarness(controller, {
922
- width: 72,
923
- height: 12,
924
- }), {
925
- width: 72,
926
- height: 12,
927
- });
928
- });
929
-
930
- await flushFrame();
931
-
932
- await emitKeypress({ name: "down", sequence: "\u001b[B" });
933
- await flushFrame();
934
-
935
- const frame = testSetup.captureCharFrame();
936
- expect(frame).toContain("Type a message...");
937
- expect(frame).not.toContain("replying to @user1");
938
- expect(frame).not.toContain("Reply to @user1...");
939
- });
940
-
941
- test("up arrow selects the newest message first when nothing is selected", async () => {
942
- const controller = createController({
943
- messages: [makeMessage(1), makeMessage(2)],
944
- });
945
-
946
- await act(async () => {
947
- testSetup = await testRender(createHarness(controller, {
948
- width: 72,
949
- height: 12,
950
- }), {
951
- width: 72,
952
- height: 12,
953
- });
954
- });
955
-
956
- await flushFrame();
957
-
958
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
959
- await emitKeypress({ name: "return", sequence: "\r" });
960
- await flushFrame();
961
-
962
- const frame = testSetup.captureCharFrame();
963
- expect(frame).toContain("replying to @user2");
964
- expect(frame).toContain("Reply to @user2...");
965
- });
966
-
967
- test("modified k does not move message selection", async () => {
968
- const controller = createController({
969
- messages: [makeMessage(1), makeMessage(2)],
970
- });
971
-
972
- await act(async () => {
973
- testSetup = await testRender(createHarness(controller, {
974
- width: 72,
975
- height: 12,
976
- }), {
977
- width: 72,
978
- height: 12,
979
- });
980
- });
981
-
982
- await flushFrame();
983
-
984
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
985
- await emitKeypress({ name: "k", sequence: "k", meta: true });
986
- await emitKeypress({ name: "return", sequence: "\r" });
987
- await flushFrame();
988
-
989
- const frame = testSetup.captureCharFrame();
990
- expect(frame).toContain("replying to @user2");
991
- expect(frame).toContain("Reply to @user2...");
992
- expect(frame).not.toContain("replying to @user1");
993
- });
994
-
995
- test("shows the reply action next to the selected message timestamp", async () => {
996
- const controller = createController({
997
- messages: [makeMessage(1), makeMessage(2)],
998
- });
999
-
1000
- await act(async () => {
1001
- testSetup = await testRender(createHarness(controller, {
1002
- width: 72,
1003
- height: 12,
1004
- }), {
1005
- width: 72,
1006
- height: 12,
1007
- });
1008
- });
1009
-
1010
- await flushFrame();
1011
-
1012
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
1013
- await flushFrame();
1014
-
1015
- const lines = testSetup.captureCharFrame().split("\n");
1016
- const headerLine = lines.find((line) => line.includes("user2"));
1017
- const bodyLine = lines.find((line) => line.includes("message 2"));
1018
-
1019
- expect(headerLine).toContain("Reply");
1020
- expect(bodyLine).not.toContain("Reply");
1021
- });
1022
-
1023
- test("down arrow from the newest selected message returns focus to the composer", async () => {
1024
- const controller = createController({
1025
- messages: [makeMessage(1), makeMessage(2)],
1026
- });
1027
-
1028
- await act(async () => {
1029
- testSetup = await testRender(createHarness(controller, {
1030
- width: 72,
1031
- height: 12,
1032
- }), {
1033
- width: 72,
1034
- height: 12,
1035
- });
1036
- });
1037
-
1038
- await flushFrame();
1039
-
1040
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
1041
- await flushFrame();
1042
- await emitKeypress({ name: "down", sequence: "\u001b[B" });
1043
- await flushFrame();
1044
-
1045
- const frame = testSetup.captureCharFrame();
1046
- expect(frame).toContain("Type a message...");
1047
- expect(frame).not.toContain("Reply to @user2...");
1048
- expect(frame).not.toContain("replying to @user2");
1049
- });
1050
-
1051
- test("up arrow leaves the composer and selects the latest message when the caret is already at the top", async () => {
1052
- const controller = createController({
1053
- messages: [makeMessage(1)],
1054
- });
1055
-
1056
- await act(async () => {
1057
- testSetup = await testRender(createHarness(controller, {
1058
- width: 72,
1059
- height: 12,
1060
- }), {
1061
- width: 72,
1062
- height: 12,
1063
- });
1064
- });
1065
-
1066
- await flushFrame();
1067
-
1068
- const lines = testSetup.captureCharFrame().split("\n");
1069
- const inputRow = lines.findIndex((line) => line.includes("Type a message..."));
1070
- const inputCol = lines[inputRow]?.indexOf("Type a message...") ?? -1;
1071
-
1072
- expect(inputRow).toBeGreaterThanOrEqual(0);
1073
- expect(inputCol).toBeGreaterThanOrEqual(0);
1074
-
1075
- await act(async () => {
1076
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1077
- await testSetup!.renderOnce();
1078
- await testSetup!.renderOnce();
1079
- });
1080
-
1081
- await act(async () => {
1082
- testSetup!.mockInput.pressArrow("up");
1083
- await testSetup!.renderOnce();
1084
- await testSetup!.renderOnce();
1085
- });
1086
- await emitKeypress({ name: "return", sequence: "\r" });
1087
- await flushFrame();
1088
-
1089
- const frame = testSetup.captureCharFrame();
1090
- expect(frame).toContain("replying to @user1");
1091
- expect(frame).toContain("Reply to @user1...");
1092
- });
1093
-
1094
- test("up arrow targets the newest bottom message when multiple messages share a timestamp", async () => {
1095
- const controller = createController({
1096
- messages: [
1097
- {
1098
- id: "z-older",
1099
- channelId: "everyone",
1100
- content: "older same timestamp",
1101
- replyToId: null,
1102
- createdAt: "2026-03-30T00:00:01.000Z",
1103
- user: { id: "u1", username: "older", displayName: "Older" },
1104
- },
1105
- {
1106
- id: "a-newer",
1107
- channelId: "everyone",
1108
- content: "newer same timestamp",
1109
- replyToId: null,
1110
- createdAt: "2026-03-30T00:00:01.000Z",
1111
- user: { id: "u2", username: "newer", displayName: "Newer" },
1112
- },
1113
- ],
1114
- });
1115
-
1116
- await act(async () => {
1117
- testSetup = await testRender(createHarness(controller, {
1118
- width: 72,
1119
- height: 12,
1120
- }), {
1121
- width: 72,
1122
- height: 12,
1123
- });
1124
- });
1125
-
1126
- await flushFrame();
1127
-
1128
- const lines = testSetup.captureCharFrame().split("\n");
1129
- const inputRow = lines.findIndex((line) => line.includes("Type a message..."));
1130
- const inputCol = lines[inputRow]?.indexOf("Type a message...") ?? -1;
1131
-
1132
- expect(inputRow).toBeGreaterThanOrEqual(0);
1133
- expect(inputCol).toBeGreaterThanOrEqual(0);
1134
-
1135
- await act(async () => {
1136
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1137
- await testSetup!.renderOnce();
1138
- await testSetup!.renderOnce();
1139
- });
1140
-
1141
- await act(async () => {
1142
- testSetup!.mockInput.pressArrow("up");
1143
- await testSetup!.renderOnce();
1144
- await testSetup!.renderOnce();
1145
- });
1146
- await emitKeypress({ name: "return", sequence: "\r" });
1147
- await flushFrame();
1148
-
1149
- const frame = testSetup.captureCharFrame();
1150
- expect(frame).toContain("replying to @newer");
1151
- expect(frame).toContain("Reply to @newer...");
1152
- });
1153
-
1154
- test("leaves one row of clearance when scrolling a selected message into the bottom edge", () => {
1155
- const nextScrollTop = getSelectedMessageScrollTop({
1156
- scrollTop: 12,
1157
- viewportHeight: 4,
1158
- top: 14,
1159
- rowHeight: 2,
1160
- });
1161
-
1162
- expect(nextScrollTop).toBe(13);
1163
- });
1164
-
1165
- test("keeps a selected desktop message inside the scroll viewport", () => {
1166
- expect(getScrollTopForElementIntoView({
1167
- scrollTop: 120,
1168
- viewportHeight: 60,
1169
- elementTop: 170,
1170
- elementHeight: 24,
1171
- })).toBe(134);
1172
-
1173
- expect(getScrollTopForElementIntoView({
1174
- scrollTop: 120,
1175
- viewportHeight: 60,
1176
- elementTop: 92,
1177
- elementHeight: 18,
1178
- })).toBe(92);
1179
-
1180
- expect(getScrollTopForElementIntoView({
1181
- scrollTop: 120,
1182
- viewportHeight: 60,
1183
- elementTop: 132,
1184
- elementHeight: 18,
1185
- })).toBe(120);
1186
- });
1187
-
1188
- test("up arrow still moves within a multi-line draft before handing off to message selection", async () => {
1189
- const controller = createController({ sessionToken: "token-123", messages: [makeMessage(1)] });
1190
- const sentMessages: string[] = [];
1191
- apiClient.connectChannel = (() => ({
1192
- send: async (content: string) => {
1193
- sentMessages.push(content);
1194
- return {
1195
- id: `server:${sentMessages.length}`,
1196
- channelId: "everyone",
1197
- content,
1198
- replyToId: null,
1199
- createdAt: "2026-03-30T00:00:30.000Z",
1200
- user: { id: "u0", username: "vince", displayName: "Vince" },
1201
- };
1202
- },
1203
- close: () => {},
1204
- })) as typeof apiClient.connectChannel;
1205
-
1206
- await act(async () => {
1207
- testSetup = await testRender(createHarness(controller, {
1208
- width: 60,
1209
- height: 12,
1210
- }), {
1211
- width: 60,
1212
- height: 12,
1213
- });
1214
- });
1215
-
1216
- await flushFrame();
1217
-
1218
- const lines = testSetup.captureCharFrame().split("\n");
1219
- const inputRow = lines.findIndex((line) => line.includes("Type a message..."));
1220
- const inputCol = lines[inputRow]?.indexOf("Type a message...") ?? -1;
1221
-
1222
- expect(inputRow).toBeGreaterThanOrEqual(0);
1223
- expect(inputCol).toBeGreaterThanOrEqual(0);
1224
-
1225
- await act(async () => {
1226
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1227
- await testSetup!.renderOnce();
1228
- await testSetup!.renderOnce();
1229
- });
1230
-
1231
- await act(async () => {
1232
- await testSetup!.mockInput.typeText("first line");
1233
- await testSetup!.renderOnce();
1234
- });
1235
-
1236
- await emitKeypress({ name: "return", sequence: "\r", shift: true });
1237
-
1238
- await act(async () => {
1239
- await testSetup!.mockInput.typeText("second line");
1240
- await testSetup!.renderOnce();
1241
- await testSetup!.renderOnce();
1242
- });
1243
-
1244
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
1245
-
1246
- await act(async () => {
1247
- testSetup!.mockInput.pressEnter();
1248
- await testSetup!.renderOnce();
1249
- await testSetup!.renderOnce();
1250
- });
1251
-
1252
- expect(sentMessages).toEqual(["first line\nsecond line"]);
1253
- expect(testSetup.captureCharFrame()).not.toContain("replying to @user1");
1254
- });
1255
-
1256
- test("shows a clear reply composer state when a reply target is active", async () => {
1257
- const controller = createController({
1258
- messages: [makeMessage(1)],
1259
- replyToId: "m1",
1260
- });
1261
-
1262
- await act(async () => {
1263
- testSetup = await testRender(createHarness(controller, {
1264
- width: 72,
1265
- height: 12,
1266
- }), {
1267
- width: 72,
1268
- height: 12,
1269
- });
1270
- });
1271
-
1272
- await flushFrame();
1273
-
1274
- const frame = testSetup.captureCharFrame();
1275
- expect(frame).toContain("replying to @user1");
1276
- expect(frame).toContain("Cancel");
1277
- expect(frame).toContain("Reply to @user1...");
1278
- });
1279
-
1280
- test("uses selected text colors for selected message rows", async () => {
1281
- const controller = createController({
1282
- messages: [makeMessage(1), makeMessage(2)],
1283
- });
1284
-
1285
- await act(async () => {
1286
- testSetup = await testRender(createHarness(controller, {
1287
- width: 72,
1288
- height: 12,
1289
- }), {
1290
- width: 72,
1291
- height: 12,
1292
- });
1293
- });
1294
-
1295
- await flushFrame();
1296
-
1297
- await emitKeypress({ name: "up", sequence: "\u001b[A" });
1298
- await flushFrame();
1299
-
1300
- const expectedSelectedFg = hexToRgbaInts(colors.selectedText);
1301
- const frame = testSetup.captureSpans();
1302
- const headerLine = frame.lines.find((line) => lineText(line).includes("user2"));
1303
- const bodyLine = frame.lines.find((line) => lineText(line).includes("message 2"));
1304
- const headerSpan = headerLine?.spans.find((span) => span.text.includes("user2"));
1305
- const bodySpan = bodyLine?.spans.find((span) => span.text.includes("message 2"));
1306
-
1307
- expect(headerSpan).toBeDefined();
1308
- expect(bodySpan).toBeDefined();
1309
- expect(headerSpan!.fg.toInts().join(",")).toBe(expectedSelectedFg);
1310
- expect(bodySpan!.fg.toInts().join(",")).toBe(expectedSelectedFg);
1311
- });
1312
-
1313
- test("does not use escape as a pane-close shortcut", async () => {
1314
- const controller = createController({
1315
- messages: [makeMessage(1)],
1316
- });
1317
- let closeCalls = 0;
1318
-
1319
- await act(async () => {
1320
- testSetup = await testRender(createHarness(controller, {
1321
- width: 72,
1322
- height: 12,
1323
- close: () => {
1324
- closeCalls += 1;
1325
- },
1326
- }), {
1327
- width: 72,
1328
- height: 12,
1329
- });
1330
- });
1331
-
1332
- await flushFrame();
1333
-
1334
- await act(async () => {
1335
- testSetup!.mockInput.pressKey("escape");
1336
- await testSetup!.renderOnce();
1337
- await testSetup!.renderOnce();
1338
- });
1339
-
1340
- expect(closeCalls).toBe(0);
1341
- });
1342
-
1343
- test("grows the composer for multi-line drafts", async () => {
1344
- const controller = createController();
1345
-
1346
- await act(async () => {
1347
- testSetup = await testRender(createHarness(controller, {
1348
- width: 32,
1349
- height: 12,
1350
- }), {
1351
- width: 32,
1352
- height: 12,
1353
- });
1354
- });
1355
-
1356
- await flushFrame();
1357
-
1358
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
1359
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
1360
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
1361
-
1362
- expect(inputRow).toBeGreaterThanOrEqual(0);
1363
- expect(inputCol).toBeGreaterThanOrEqual(0);
1364
-
1365
- await act(async () => {
1366
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1367
- await testSetup!.renderOnce();
1368
- await testSetup!.renderOnce();
1369
- });
1370
-
1371
- await act(async () => {
1372
- await testSetup!.mockInput.typeText("alpha bravo");
1373
- await testSetup!.renderOnce();
1374
- });
1375
-
1376
- await emitKeypress({ name: "return", sequence: "\r", shift: true });
1377
-
1378
- await act(async () => {
1379
- await testSetup!.mockInput.typeText("charlie delta");
1380
- await testSetup!.renderOnce();
1381
- });
1382
-
1383
- await emitKeypress({ name: "return", sequence: "\r", shift: true });
1384
-
1385
- await act(async () => {
1386
- await testSetup!.mockInput.typeText("echo foxtrot");
1387
- await testSetup!.renderOnce();
1388
- await testSetup!.renderOnce();
1389
- });
1390
-
1391
- const rows = testSetup.captureCharFrame().split("\n");
1392
- const firstRow = rows.findIndex((line) => line.includes("alpha bravo"));
1393
- const secondRow = rows.findIndex((line) => line.includes("charlie delta"));
1394
- const thirdRow = rows.findIndex((line) => line.includes("echo foxtrot"));
1395
-
1396
- expect(firstRow).toBeGreaterThanOrEqual(0);
1397
- expect(secondRow).toBeGreaterThan(firstRow);
1398
- expect(thirdRow).toBeGreaterThan(secondRow);
1399
- });
1400
-
1401
- test("keeps Enter as send and uses Shift+Enter for composer newlines", async () => {
1402
- const controller = createController({ sessionToken: "token-123" });
1403
- const sentMessages: string[] = [];
1404
- apiClient.connectChannel = (() => ({
1405
- send: async (content: string) => {
1406
- sentMessages.push(content);
1407
- return {
1408
- id: `server:${sentMessages.length}`,
1409
- channelId: "everyone",
1410
- content,
1411
- replyToId: null,
1412
- createdAt: "2026-03-30T00:00:30.000Z",
1413
- user: { id: "u0", username: "vince", displayName: "Vince" },
1414
- };
1415
- },
1416
- close: () => {},
1417
- })) as typeof apiClient.connectChannel;
1418
-
1419
- await act(async () => {
1420
- testSetup = await testRender(createHarness(controller), {
1421
- width: 60,
1422
- height: 12,
1423
- });
1424
- });
1425
-
1426
- await flushFrame();
1427
-
1428
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
1429
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
1430
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
1431
-
1432
- expect(inputRow).toBeGreaterThanOrEqual(0);
1433
- expect(inputCol).toBeGreaterThanOrEqual(0);
1434
-
1435
- await act(async () => {
1436
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1437
- await testSetup!.renderOnce();
1438
- await testSetup!.renderOnce();
1439
- });
1440
-
1441
- await act(async () => {
1442
- await testSetup!.mockInput.typeText("first line");
1443
- await testSetup!.renderOnce();
1444
- });
1445
-
1446
- await emitKeypress({ name: "return", sequence: "\r", shift: true });
1447
-
1448
- await act(async () => {
1449
- await testSetup!.mockInput.typeText("second line");
1450
- await testSetup!.renderOnce();
1451
- await testSetup!.renderOnce();
1452
- });
1453
-
1454
- const frameAfterNewline = testSetup.captureCharFrame();
1455
- expect(frameAfterNewline).toContain("first line");
1456
- expect(frameAfterNewline).toContain("second line");
1457
- expect(sentMessages).toEqual([]);
1458
-
1459
- await act(async () => {
1460
- testSetup!.mockInput.pressEnter();
1461
- await testSetup!.renderOnce();
1462
- await testSetup!.renderOnce();
1463
- });
1464
-
1465
- expect(sentMessages).toEqual(["first line\nsecond line"]);
1466
- });
1467
-
1468
- test("clears the composer locally after an accepted send", async () => {
1469
- const controller = createController({ sessionToken: "token-123" });
1470
- const sentMessages: string[] = [];
1471
- (controller as any).send = (content: string) => {
1472
- sentMessages.push(content);
1473
- return true;
1474
- };
1475
- (controller as any).setDraft = () => {};
1476
-
1477
- await act(async () => {
1478
- testSetup = await testRender(createHarness(controller), {
1479
- width: 60,
1480
- height: 12,
1481
- });
1482
- });
1483
-
1484
- await flushFrame();
1485
-
1486
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
1487
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
1488
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
1489
-
1490
- expect(inputRow).toBeGreaterThanOrEqual(0);
1491
- expect(inputCol).toBeGreaterThanOrEqual(0);
1492
-
1493
- await act(async () => {
1494
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1495
- await testSetup!.renderOnce();
1496
- await testSetup!.renderOnce();
1497
- });
1498
-
1499
- await act(async () => {
1500
- await testSetup!.mockInput.typeText("hello");
1501
- await testSetup!.renderOnce();
1502
- await testSetup!.renderOnce();
1503
- });
1504
-
1505
- await act(async () => {
1506
- testSetup!.mockInput.pressEnter();
1507
- await testSetup!.renderOnce();
1508
- await testSetup!.renderOnce();
1509
- });
1510
-
1511
- expect(sentMessages).toEqual(["hello"]);
1512
- expect(testSetup.captureCharFrame()).not.toContain("> hello");
1513
-
1514
- await act(async () => {
1515
- testSetup!.mockInput.pressEnter();
1516
- await testSetup!.renderOnce();
1517
- await testSetup!.renderOnce();
1518
- });
1519
-
1520
- expect(sentMessages).toEqual(["hello"]);
1521
- });
1522
-
1523
- test("keeps typed shortcut letters in the composer instead of moving message selection", async () => {
1524
- const controller = createController({
1525
- messages: Array.from({ length: 18 }, (_, index) => makeMessage(index + 1)),
1526
- });
1527
-
1528
- await act(async () => {
1529
- testSetup = await testRender(createHarness(controller), {
1530
- width: 60,
1531
- height: 12,
1532
- });
1533
- });
1534
-
1535
- await flushFrame();
1536
-
1537
- const frameBeforeClick = testSetup.captureCharFrame().split("\n");
1538
- const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
1539
- const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
1540
-
1541
- expect(inputRow).toBeGreaterThanOrEqual(0);
1542
- expect(inputCol).toBeGreaterThanOrEqual(0);
1543
-
1544
- await act(async () => {
1545
- await testSetup!.mockMouse.click(inputCol + 1, inputRow);
1546
- await testSetup!.renderOnce();
1547
- await testSetup!.renderOnce();
1548
- });
1549
-
1550
- await act(async () => {
1551
- await testSetup!.mockInput.typeText("g");
1552
- await testSetup!.renderOnce();
1553
- await testSetup!.renderOnce();
1554
- });
1555
-
1556
- const frameAfterType = testSetup.captureCharFrame();
1557
- expect(frameAfterType).toContain("> g");
1558
- expect(frameAfterType).not.toContain("user1 3/30/26");
1559
- expect(frameAfterType).not.toContain("message 1 ");
1560
- });
1561
-
1562
- test("renders optimistic sends with a sending status", async () => {
1563
- const controller = createController({
1564
- messages: [{
1565
- id: "local:1",
1566
- channelId: "everyone",
1567
- content: "hello",
1568
- replyToId: null,
1569
- createdAt: "2026-03-28T00:00:00.000Z",
1570
- user: { id: "u0", username: "vince", displayName: "Vince" },
1571
- clientStatus: "sending",
1572
- clientError: null,
1573
- }],
1574
- });
1575
-
1576
- await act(async () => {
1577
- testSetup = await testRender(createHarness(controller), {
1578
- width: 60,
1579
- height: 12,
1580
- });
1581
- });
1582
-
1583
- await flushFrame();
1584
-
1585
- const frameAfterSubmit = testSetup.captureCharFrame();
1586
- expect(frameAfterSubmit).toContain("hello");
1587
- expect(frameAfterSubmit).toContain("sending...");
1588
- });
1589
-
1590
- test("auto-scrolls to newly appended messages while following the latest transcript", async () => {
1591
- const controller = createController({
1592
- messages: [
1593
- makeMessage(1),
1594
- makeMessage(2),
1595
- makeMessage(3),
1596
- makeMessage(4),
1597
- makeMessage(5),
1598
- makeMessage(6),
1599
- ],
1600
- });
1601
-
1602
- await act(async () => {
1603
- testSetup = await testRender(createHarness(controller, { width: 60, height: 13, withFooter: true }), {
1604
- width: 60,
1605
- height: 13,
1606
- });
1607
- });
1608
-
1609
- await flushFrame();
1610
-
1611
- const frameBeforeUpdate = testSetup.captureCharFrame();
1612
- expect(frameBeforeUpdate).toContain("message 6");
1613
- expect(frameBeforeUpdate).not.toContain("message 1");
1614
-
1615
- await act(async () => {
1616
- (controller as any).mergeMessages([makeMessage(7)]);
1617
- });
1618
-
1619
- await flushFrame();
1620
-
1621
- const frameAfterUpdate = testSetup.captureCharFrame();
1622
- expect(frameAfterUpdate).not.toContain("7 messages");
1623
- expect(frameAfterUpdate).toContain("message 7");
1624
- expect(frameAfterUpdate).not.toContain("message 1");
1625
- });
1626
-
1627
- test("loads older messages at the top without jumping away from the current transcript", async () => {
1628
- const controller = createController({
1629
- messages: [
1630
- makeMessage(4),
1631
- makeMessage(5),
1632
- makeMessage(6),
1633
- makeMessage(7),
1634
- makeMessage(8),
1635
- makeMessage(9),
1636
- makeMessage(10),
1637
- ],
1638
- });
1639
- let loadCount = 0;
1640
- controller.loadOlderMessages = async () => {
1641
- loadCount += 1;
1642
- (controller as any).mergeMessages([
1643
- makeMessage(1),
1644
- makeMessage(2),
1645
- makeMessage(3),
1646
- ], { notifyMentions: false });
1647
- };
1648
-
1649
- await act(async () => {
1650
- testSetup = await testRender(createHarness(controller, { width: 60, height: 13 }), {
1651
- width: 60,
1652
- height: 13,
1653
- });
1654
- });
1655
-
1656
- await flushFrame();
1657
- expect(testSetup.captureCharFrame()).toContain("message 10");
1658
-
1659
- await emitKeypress({ name: "g", sequence: "g" });
1660
- await flushFrame();
1661
-
1662
- expect(loadCount).toBe(1);
1663
- expect(controller.getSnapshot().messages.map((message) => message.id)).toEqual([
1664
- "m1",
1665
- "m2",
1666
- "m3",
1667
- "m4",
1668
- "m5",
1669
- "m6",
1670
- "m7",
1671
- "m8",
1672
- "m9",
1673
- "m10",
1674
- ]);
1675
- const frame = testSetup.captureCharFrame();
1676
- expect(frame).toContain("message 4");
1677
- expect(frame).not.toContain("message 1");
1678
- });
1679
-
1680
- test("preserves a manually scrolled transcript across focus and snapshot refreshes", async () => {
1681
- const controller = createController({
1682
- messages: [
1683
- makeMessage(1),
1684
- makeMessage(2),
1685
- makeMessage(3),
1686
- makeMessage(4),
1687
- makeMessage(5),
1688
- makeMessage(6),
1689
- makeMessage(7),
1690
- makeMessage(8),
1691
- ],
1692
- });
1693
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat-refocus"));
1694
- let setFocused: ((focused: boolean) => void) | undefined;
1695
-
1696
- function FocusHarness() {
1697
- const [isFocused, updateFocused] = useState(false);
1698
- setFocused = updateFocused;
1699
-
1700
- return (
1701
- <AppContext value={{ state, dispatch: () => {} }}>
1702
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
1703
- <ChatContent
1704
- controller={controller}
1705
- width={60}
1706
- height={13}
1707
- focused={isFocused}
1708
- />
1709
- </PluginRenderProvider>
1710
- </AppContext>
1711
- );
1712
- }
1713
-
1714
- await act(async () => {
1715
- testSetup = await testRender(<FocusHarness />, {
1716
- width: 60,
1717
- height: 13,
1718
- });
1719
- });
1720
-
1721
- await flushFrame();
1722
- expect(testSetup.captureCharFrame()).toContain("message 8");
1723
-
1724
- await act(async () => {
1725
- await testSetup!.mockMouse.scroll(3, 4, "up");
1726
- await testSetup!.mockMouse.scroll(3, 4, "up");
1727
- await testSetup!.mockMouse.scroll(3, 4, "up");
1728
- await testSetup!.renderOnce();
1729
- await testSetup!.renderOnce();
1730
- });
1731
-
1732
- const scrolledFrame = testSetup.captureCharFrame();
1733
- expect(scrolledFrame).toContain("message 2");
1734
- expect(scrolledFrame).not.toContain("message 8");
1735
-
1736
- await act(async () => {
1737
- setFocused?.(true);
1738
- (controller as any).emit("everyone");
1739
- await testSetup!.renderOnce();
1740
- });
1741
- await flushFrame();
1742
-
1743
- const refreshedFrame = testSetup.captureCharFrame();
1744
- expect(refreshedFrame).toContain("message 2");
1745
- expect(refreshedFrame).not.toContain("message 8");
1746
- });
1747
-
1748
- test("opens a newly selected channel at the latest message", async () => {
1749
- const controller = createController({
1750
- messages: Array.from({ length: 8 }, (_, index) => makeMessage(index + 1)),
1751
- });
1752
- installServerChannels(controller, TEST_CHAT_CHANNELS.slice(0, 2));
1753
- controller.refreshChannels = async () => {};
1754
- controller.refreshChannelMessages = async () => {};
1755
-
1756
- const equitiesMessages = Array.from({ length: 8 }, (_, index) => ({
1757
- ...makeMessage(index + 1),
1758
- id: `o${index + 1}`,
1759
- channelId: "equities",
1760
- content: `equities message ${index + 1}`,
1761
- }));
1762
- const equitiesState = (controller as any).ensureChannelState("equities");
1763
- equitiesState.messages = equitiesMessages;
1764
- equitiesState.lastCursor = "o8";
1765
-
1766
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat-channel-scroll"));
1767
-
1768
- function ChannelPane() {
1769
- const [channelId, setChannelId] = useState("everyone");
1770
- return (
1771
- <AppContext value={{ state, dispatch: () => {} }}>
1772
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
1773
- <ChatContent
1774
- controller={controller}
1775
- width={90}
1776
- height={13}
1777
- focused
1778
- channelId={channelId}
1779
- onChannelChange={setChannelId}
1780
- />
1781
- </PluginRenderProvider>
1782
- </AppContext>
1783
- );
1784
- }
1785
-
1786
- await act(async () => {
1787
- testSetup = await testRender(<ChannelPane />, {
1788
- width: 90,
1789
- height: 13,
1790
- });
1791
- });
1792
-
1793
- await flushFrame();
1794
- expect(testSetup.captureCharFrame()).toContain("message 8");
1795
-
1796
- await act(async () => {
1797
- await testSetup!.mockMouse.scroll(24, 4, "up");
1798
- await testSetup!.mockMouse.scroll(24, 4, "up");
1799
- await testSetup!.mockMouse.scroll(24, 4, "up");
1800
- await testSetup!.renderOnce();
1801
- await testSetup!.renderOnce();
1802
- });
1803
- await flushFrame();
1804
- expect(testSetup.captureCharFrame()).toContain("message 2");
1805
-
1806
- const lines = testSetup.captureCharFrame().split("\n");
1807
- const row = lines.findIndex((line) => line.includes("equities"));
1808
- const col = lines[row]?.indexOf("equities") ?? -1;
1809
- expect(row).toBeGreaterThanOrEqual(0);
1810
- expect(col).toBeGreaterThanOrEqual(0);
1811
-
1812
- await act(async () => {
1813
- await testSetup!.mockMouse.click(col + 1, row);
1814
- await testSetup!.renderOnce();
1815
- await testSetup!.renderOnce();
1816
- });
1817
- await flushFrame();
1818
-
1819
- const channelFrame = testSetup.captureCharFrame();
1820
- expect(channelFrame).toContain("equities message 8");
1821
- expect(channelFrame).not.toContain("equities message 1");
1822
- });
1823
-
1824
- test("keeps the bottom reachable after narrowing the chat pane", async () => {
1825
- const controller = createController({
1826
- messages: [
1827
- {
1828
- ...makeMessage(1),
1829
- content: "A wide pane can fit this update in fewer rows, but a narrow pane needs several wrapped rows before the final message.",
1830
- },
1831
- {
1832
- ...makeMessage(2),
1833
- content: "Another wrapped message takes up more transcript height after the pane gets smaller.",
1834
- },
1835
- {
1836
- ...makeMessage(3),
1837
- user: { id: "u3", username: "vince", displayName: "Vince" },
1838
- content: "newest message stays reachable after shrink",
1839
- },
1840
- ],
1841
- });
1842
-
1843
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat-resizable"));
1844
- let resize: ((size: { width: number; height: number }) => void) | undefined;
1845
-
1846
- function ResizableChatHarness() {
1847
- const [size, setSize] = useState({ width: 78, height: 12 });
1848
- resize = setSize;
1849
-
1850
- return (
1851
- <AppContext value={{ state, dispatch: () => {} }}>
1852
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
1853
- <ChatContent
1854
- controller={controller}
1855
- width={size.width}
1856
- height={size.height}
1857
- focused
1858
- />
1859
- </PluginRenderProvider>
1860
- </AppContext>
1861
- );
1862
- }
1863
-
1864
- await act(async () => {
1865
- testSetup = await testRender(<ResizableChatHarness />, {
1866
- width: 78,
1867
- height: 12,
1868
- });
1869
- });
1870
-
1871
- await flushFrame();
1872
-
1873
- await act(async () => {
1874
- resize?.({ width: 42, height: 9 });
1875
- testSetup!.resize(42, 9);
1876
- });
1877
- await flushFrame();
1878
-
1879
- const frame = testSetup!.captureCharFrame();
1880
- expect(frame).toContain("vince");
1881
- expect(frame).toContain("newest message stays");
1882
- expect(frame).toContain("shrink");
1883
- });
1884
-
1885
- test("renders ticker badges and opens a floating detail pane on click", async () => {
1886
- const controller = createController({
1887
- messages: [{
1888
- id: "m1",
1889
- channelId: "everyone",
1890
- content: "Watching $TSLA today",
1891
- replyToId: null,
1892
- createdAt: "2026-03-28T00:00:00.000Z",
1893
- user: { id: "u1", username: "vince", displayName: "Vince" },
1894
- }],
1895
- });
1896
- const opened: string[] = [];
1897
-
1898
- setSharedRegistryForTests({
1899
- pinTicker(symbol: string) {
1900
- opened.push(symbol);
1901
- },
1902
- } as any);
1903
-
1904
- await act(async () => {
1905
- testSetup = await testRender(createHarness(controller, {
1906
- width: 60,
1907
- height: 12,
1908
- configureState(state) {
1909
- state.tickers = new Map([["TSLA", {
1910
- metadata: {
1911
- ticker: "TSLA",
1912
- exchange: "NASDAQ",
1913
- currency: "USD",
1914
- name: "Tesla, Inc.",
1915
- portfolios: [],
1916
- watchlists: [],
1917
- positions: [],
1918
- custom: {},
1919
- tags: [],
1920
- },
1921
- }]]);
1922
- state.financials = new Map([["TSLA", {
1923
- annualStatements: [],
1924
- quarterlyStatements: [],
1925
- priceHistory: [],
1926
- quote: {
1927
- symbol: "TSLA",
1928
- price: 250,
1929
- currency: "USD",
1930
- change: -12.5,
1931
- changePercent: -5,
1932
- lastUpdated: Date.now(),
1933
- },
1934
- }]]);
1935
- },
1936
- }), {
1937
- width: 60,
1938
- height: 12,
1939
- });
1940
- });
1941
-
1942
- await flushFrame();
1943
-
1944
- const lines = testSetup.captureCharFrame().split("\n");
1945
- const row = lines.findIndex((line) => line.includes("TSLA -5%"));
1946
- const col = lines[row]?.indexOf("TSLA -5%") ?? -1;
1947
-
1948
- expect(row).toBeGreaterThanOrEqual(0);
1949
- expect(col).toBeGreaterThanOrEqual(0);
1950
-
1951
- await act(async () => {
1952
- await testSetup!.mockMouse.click(col + 1, row);
1953
- await testSetup!.renderOnce();
1954
- await testSetup!.renderOnce();
1955
- });
1956
-
1957
- expect(opened).toEqual(["TSLA"]);
1958
- });
1959
-
1960
- test("renders detected links in chat messages", async () => {
1961
- const controller = createController({
1962
- messages: [{
1963
- id: "m1",
1964
- channelId: "everyone",
1965
- content: "Read https://example.com/story.",
1966
- replyToId: null,
1967
- createdAt: "2026-03-28T00:00:00.000Z",
1968
- user: { id: "u1", username: "vince", displayName: "Vince" },
1969
- }],
1970
- });
1971
-
1972
- await act(async () => {
1973
- testSetup = await testRender(createHarness(controller, {
1974
- width: 60,
1975
- height: 12,
1976
- }), {
1977
- width: 60,
1978
- height: 12,
1979
- });
1980
- });
1981
-
1982
- await flushFrame();
1983
-
1984
- const frame = testSetup.captureCharFrame();
1985
- expect(frame).toContain("https://example.com/story.");
1986
- });
1987
-
1988
- test("shows a saved-login read-only footer when a session token is cached", async () => {
1989
- const controller = createController({
1990
- sessionToken: "token-123",
1991
- user: null,
1992
- });
1993
-
1994
- await act(async () => {
1995
- testSetup = await testRender(createHarness(controller), {
1996
- width: 60,
1997
- height: 12,
1998
- });
1999
- });
2000
-
2001
- await flushFrame();
2002
-
2003
- const frame = testSetup.captureCharFrame();
2004
- expect(frame).toContain("Saved login found.");
2005
- expect(frame).toContain("Log in again to send.");
2006
- expect(frame).toContain("No messages yet.");
2007
- expect(frame).not.toContain("Type a message...");
2008
- });
2009
-
2010
- test("keeps the transcript visible for logged-out users and blocks the composer", async () => {
2011
- const controller = createController({
2012
- sessionToken: null,
2013
- user: null,
2014
- messages: [makeMessage(1)],
2015
- });
2016
-
2017
- await act(async () => {
2018
- testSetup = await testRender(createHarness(controller), {
2019
- width: 60,
2020
- height: 12,
2021
- });
2022
- });
2023
-
2024
- await flushFrame();
2025
-
2026
- const frame = testSetup.captureCharFrame();
2027
- expect(frame).toContain("message 1");
2028
- expect(frame).toContain("Read-only chat.");
2029
- expect(frame).toContain("Log In");
2030
- expect(frame).toContain("Sign Up");
2031
- expect(frame).not.toContain("Type a message...");
2032
- });
2033
-
2034
- test("shows a logged-in icon in the cloud status widget for cached sessions", async () => {
2035
- const controller = createController({
2036
- sessionToken: "token-123",
2037
- user: null,
2038
- });
2039
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
2040
- state.config.disabledPlugins = [];
2041
-
2042
- await act(async () => {
2043
- testSetup = await testRender(
2044
- <AppContext value={{ state, dispatch: () => {} }}>
2045
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
2046
- <ChatStatusWidget controller={controller} />
2047
- </PluginRenderProvider>
2048
- </AppContext>,
2049
- { width: 40, height: 1 },
2050
- );
2051
- });
2052
-
2053
- await flushFrame();
2054
-
2055
- const frame = testSetup.captureCharFrame();
2056
- expect(frame).toContain("@");
2057
- expect(frame).not.toContain("Shift+C");
2058
- expect(frame).not.toContain("vince");
2059
- });
2060
-
2061
- test("shows clickable login actions instead of the cloud shortcut when logged out", async () => {
2062
- const controller = createController({
2063
- sessionToken: null,
2064
- user: null,
2065
- });
2066
- const openedQueries: string[] = [];
2067
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
2068
- state.config.disabledPlugins = [];
2069
-
2070
- const runtime = createTestPluginRuntime({
2071
- openCommandBar(query?: string) {
2072
- openedQueries.push(query ?? "");
2073
- },
2074
- });
2075
-
2076
- await act(async () => {
2077
- testSetup = await testRender(
2078
- <AppContext value={{ state, dispatch: () => {} }}>
2079
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={runtime}>
2080
- <ChatStatusWidget controller={controller} />
2081
- </PluginRenderProvider>
2082
- </AppContext>,
2083
- { width: 40, height: 1 },
2084
- );
2085
- });
2086
-
2087
- await flushFrame();
2088
-
2089
- const frame = testSetup.captureCharFrame();
2090
- expect(frame).toContain("☁");
2091
- expect(frame).toContain("Log In");
2092
- expect(frame).not.toContain("Sign Up");
2093
- expect(frame).not.toContain("Shift+C");
2094
-
2095
- const line = frame.split("\n")[0] ?? "";
2096
- const loginCol = line.indexOf("Log In");
2097
-
2098
- expect(loginCol).toBeGreaterThanOrEqual(0);
2099
-
2100
- await act(async () => {
2101
- await testSetup!.mockMouse.click(loginCol + 1, 0);
2102
- await testSetup!.renderOnce();
2103
- await testSetup!.renderOnce();
2104
- });
2105
-
2106
- expect(openedQueries).toEqual(["Log In"]);
2107
- });
2108
-
2109
- test("shows an unread mention badge and opens chat from the status widget", async () => {
2110
- const controller = createController({
2111
- sessionToken: "token-123",
2112
- user: { id: "u1", username: "vince", emailVerified: true },
2113
- });
2114
- const openedPanes: string[] = [];
2115
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
2116
- state.config.disabledPlugins = [];
2117
-
2118
- const runtime = createTestPluginRuntime({
2119
- showPane(paneId: string) {
2120
- openedPanes.push(paneId);
2121
- },
2122
- });
2123
-
2124
- await act(async () => {
2125
- (controller as any).mergeMessages([{
2126
- id: "m1",
2127
- channelId: "everyone",
2128
- content: "pinging @vince before the bell",
2129
- replyToId: null,
2130
- createdAt: "2026-03-28T00:00:00.000Z",
2131
- user: { id: "u2", username: "bob", displayName: "Bob" },
2132
- } satisfies ChatMessage]);
2133
- });
2134
-
2135
- await act(async () => {
2136
- testSetup = await testRender(
2137
- <AppContext value={{ state, dispatch: () => {} }}>
2138
- <PluginRenderProvider pluginId="gloomberb-cloud" runtime={runtime}>
2139
- <ChatStatusWidget controller={controller} />
2140
- </PluginRenderProvider>
2141
- </AppContext>,
2142
- { width: 40, height: 1 },
2143
- );
2144
- });
2145
-
2146
- await flushFrame();
2147
-
2148
- const frame = testSetup.captureCharFrame();
2149
- expect(frame).toContain("vince");
2150
- expect(frame).toContain("[1]");
2151
-
2152
- const line = frame.split("\n")[0] ?? "";
2153
- const badgeCol = line.indexOf("[1]");
2154
-
2155
- expect(badgeCol).toBeGreaterThanOrEqual(0);
2156
-
2157
- await act(async () => {
2158
- await testSetup!.mockMouse.click(badgeCol + 1, 0);
2159
- await testSetup!.renderOnce();
2160
- await testSetup!.renderOnce();
2161
- });
2162
-
2163
- expect(openedPanes).toEqual(["chat"]);
2164
- });
2165
-
2166
- test("auth commands use a single-form layout and signup no longer prompts for display name", () => {
2167
- const registeredCommands: Array<{ id: string; wizardLayout?: string; wizard?: Array<{ key: string }> }> = [];
2168
-
2169
- gloomberbCloudPlugin.setup({
2170
- persistence: new MemoryPersistence(),
2171
- resume: {
2172
- getState: () => null,
2173
- setState: () => {},
2174
- deleteState: () => {},
2175
- },
2176
- registerPane: () => {},
2177
- registerPaneTemplate: () => {},
2178
- registerDetailTab: () => {},
2179
- registerShortcut: () => {},
2180
- registerCommand: (command: { id: string; wizardLayout?: string; wizard?: Array<{ key: string }> }) => {
2181
- registeredCommands.push(command);
2182
- },
2183
- showPane: () => {},
2184
- hidePane: () => {},
2185
- notify: () => {},
2186
- } as any);
2187
-
2188
- const loginCommand = registeredCommands.find((command) => command.id === "auth-login");
2189
- const signupCommand = registeredCommands.find((command) => command.id === "auth-signup");
2190
-
2191
- expect(loginCommand?.wizardLayout).toBe("form");
2192
- expect(signupCommand?.wizardLayout).toBe("form");
2193
- expect(signupCommand?.wizard?.map((step) => step.key)).toEqual([
2194
- "email",
2195
- "username",
2196
- "password",
2197
- "confirmPassword",
2198
- "_validate",
2199
- ]);
2200
- });
2201
-
2202
- test("new chat pane opens directly on the last visited channel", async () => {
2203
- const originalRefreshChannels = chatController.refreshChannels;
2204
- installServerChannels(chatController);
2205
- chatController.refreshChannels = async () => {};
2206
- const chatTemplate = gloomberbCloudPlugin.paneTemplates?.find((template) => template.id === "new-chat-pane");
2207
- try {
2208
- expect(chatTemplate?.wizard).toBeUndefined();
2209
- expect(chatTemplate?.shortcut).toMatchObject({
2210
- prefix: "CHAT",
2211
- argPlaceholder: "channel",
2212
- argKind: "text",
2213
- });
2214
- const created = await Promise.resolve(chatTemplate?.createInstance?.({
2215
- config: {
2216
- pluginConfig: {
2217
- "gloomberb-cloud": {
2218
- lastChatChannelId: "options",
2219
- },
2220
- },
2221
- },
2222
- }));
2223
- expect(created).toMatchObject({
2224
- settings: { channelId: "options" },
2225
- placement: "floating",
2226
- });
2227
- const createdFromShortcut = await Promise.resolve(chatTemplate?.createInstance?.({
2228
- config: {
2229
- pluginConfig: {
2230
- "gloomberb-cloud": {
2231
- lastChatChannelId: "options",
2232
- },
2233
- },
2234
- },
2235
- }, { arg: "#HELP" }));
2236
- expect(createdFromShortcut).toMatchObject({
2237
- settings: { channelId: "help" },
2238
- placement: "floating",
2239
- });
2240
- } finally {
2241
- chatController.refreshChannels = originalRefreshChannels;
2242
- installServerChannels(chatController, []);
2243
- }
2244
- });
2245
- });