gloomberb 0.9.9 → 0.10.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 (519) hide show
  1. package/README.md +41 -3
  2. package/README.zh-CN.md +294 -0
  3. package/package.json +10 -4
  4. package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
  5. package/src/api-client/auth.ts +2 -0
  6. package/src/api-client/index.test.ts +4 -0
  7. package/src/api-client/types.ts +3 -0
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
  9. package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
  10. package/src/app/pane-template-dialog-wizard.tsx +10 -3
  11. package/src/app/runtime/desktop-deeplink.test.ts +15 -0
  12. package/src/app/runtime/desktop-deeplink.ts +21 -6
  13. package/src/app.tsx +40 -33
  14. package/src/capabilities/service-capabilities.ts +7 -2
  15. package/src/cli/commands/automation.test.ts +202 -0
  16. package/src/cli/commands/automation.ts +153 -36
  17. package/src/cli/commands/market.ts +1 -14
  18. package/src/cli/commands/system.ts +4 -1
  19. package/src/cli/context.ts +1 -1
  20. package/src/cli/desktop-pane-shot.ts +65 -4
  21. package/src/cli/entry.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +532 -0
  23. package/src/cli/pane-functions/catalog.ts +39 -4
  24. package/src/cli/pane-functions/data.test.ts +17 -0
  25. package/src/cli/pane-functions/data.ts +55 -13
  26. package/src/cli/pane-functions/discovery.ts +36 -9
  27. package/src/cli/pane-functions/index.test.ts +104 -1
  28. package/src/cli/pane-functions/index.ts +64 -5
  29. package/src/cli/pane-functions/options.ts +21 -6
  30. package/src/cli/pane-functions/report.ts +735 -72
  31. package/src/cli/pane-functions/resolver.ts +70 -6
  32. package/src/cli/pane-functions/screenshot.test.ts +431 -0
  33. package/src/cli/pane-functions/screenshot.ts +952 -8
  34. package/src/components/chart/composite/column-layout.ts +215 -0
  35. package/src/components/chart/composite/composite-chart.test.tsx +630 -0
  36. package/src/components/chart/composite/composite-chart.tsx +1148 -0
  37. package/src/components/chart/composite/format.test.ts +137 -0
  38. package/src/components/chart/composite/format.ts +135 -0
  39. package/src/components/chart/composite/index.ts +38 -0
  40. package/src/components/chart/composite/interactions.test.ts +109 -0
  41. package/src/components/chart/composite/interactions.ts +247 -0
  42. package/src/components/chart/composite/price-series.test.ts +39 -0
  43. package/src/components/chart/composite/price-series.ts +78 -0
  44. package/src/components/chart/composite/rasterizer.ts +359 -0
  45. package/src/components/chart/composite/renderers.test.ts +610 -0
  46. package/src/components/chart/composite/scene.test.ts +464 -0
  47. package/src/components/chart/composite/scene.ts +553 -0
  48. package/src/components/chart/composite/text-renderer.ts +285 -0
  49. package/src/components/chart/composite/time-scale.test.ts +88 -0
  50. package/src/components/chart/composite/time-scale.ts +217 -0
  51. package/src/components/chart/composite/types.ts +141 -0
  52. package/src/components/chart/core/data.ts +81 -5
  53. package/src/components/chart/core/index.test.ts +128 -43
  54. package/src/components/chart/core/resolution.ts +0 -6
  55. package/src/components/chart/native/chart-rasterizer.ts +1 -3
  56. package/src/components/chart/native/kitty/index.test.ts +99 -120
  57. package/src/components/chart/native/kitty/manager.ts +3 -0
  58. package/src/components/chart/native/kitty/protocol.ts +4 -1
  59. package/src/components/chart/native/raster/crosshair.ts +79 -46
  60. package/src/components/chart/native/raster/types.ts +2 -11
  61. package/src/components/chart/native/surface/manager.ts +48 -3
  62. package/src/components/chart/price-axis-labels.tsx +18 -4
  63. package/src/components/command-bar/commands/direct/index.ts +25 -0
  64. package/src/components/command-bar/commands/registry.ts +9 -0
  65. package/src/components/command-bar/helpers.test.ts +4 -1
  66. package/src/components/command-bar/list/view.tsx +18 -6
  67. package/src/components/command-bar/multi-select-picker.tsx +6 -5
  68. package/src/components/command-bar/pane-settings.test.ts +141 -0
  69. package/src/components/command-bar/pane-settings.ts +61 -1
  70. package/src/components/command-bar/panel/body.tsx +7 -6
  71. package/src/components/command-bar/routes/root/command-items.ts +4 -4
  72. package/src/components/command-bar/routes/root/results.ts +10 -1
  73. package/src/components/command-bar/routes/root/selection.test.ts +54 -0
  74. package/src/components/command-bar/routes/root/selection.ts +13 -0
  75. package/src/components/command-bar/routing/actions.ts +18 -2
  76. package/src/components/command-bar/routing/list-state.ts +2 -1
  77. package/src/components/command-bar/surface/index.test.tsx +36 -4
  78. package/src/components/command-bar/surface/pane.test.tsx +93 -0
  79. package/src/components/command-bar/theme-picker.tsx +2 -1
  80. package/src/components/command-bar/view-model.ts +19 -20
  81. package/src/components/command-bar/workflow/body.tsx +2 -1
  82. package/src/components/command-bar/workflow/field-row.tsx +3 -2
  83. package/src/components/command-bar/workflow/fields.ts +4 -2
  84. package/src/components/command-bar/workflow/ops.test.ts +161 -0
  85. package/src/components/command-bar/workflow/ops.ts +20 -50
  86. package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
  87. package/src/components/command-bar/workflow/route-actions.ts +4 -0
  88. package/src/components/command-bar/workflow/types.ts +1 -0
  89. package/src/components/feed-data-table/stack-view.tsx +6 -5
  90. package/src/components/index.ts +13 -1
  91. package/src/components/layout/detached-pane-shell.tsx +2 -1
  92. package/src/components/layout/floating-pane.tsx +3 -0
  93. package/src/components/layout/header.tsx +15 -8
  94. package/src/components/layout/pane/footer/index.test.tsx +22 -3
  95. package/src/components/layout/pane/footer/index.tsx +3 -1
  96. package/src/components/layout/pane/footer/model.ts +9 -2
  97. package/src/components/layout/pane/header.tsx +4 -6
  98. package/src/components/layout/pane/index.tsx +3 -0
  99. package/src/components/layout/pane/sidebar.test.tsx +100 -0
  100. package/src/components/layout/pane/sidebar.tsx +239 -0
  101. package/src/components/layout/pane/title.ts +4 -3
  102. package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
  103. package/src/components/layout/shell/drag/runtime.ts +2 -0
  104. package/src/components/layout/shell/menu.test.ts +19 -0
  105. package/src/components/layout/shell/menu.ts +8 -5
  106. package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
  107. package/src/components/layout/shell/pane/layers.tsx +2 -2
  108. package/src/components/layout/status-bar.tsx +8 -7
  109. package/src/components/layout/window-controls.tsx +4 -3
  110. package/src/components/metric-treemap/index.tsx +3 -2
  111. package/src/components/onboarding/onboarding-steps.tsx +30 -29
  112. package/src/components/onboarding/onboarding-wizard.tsx +5 -4
  113. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
  114. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
  115. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
  116. package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
  117. package/src/components/pane-settings-dialog/desktop.tsx +25 -3
  118. package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
  119. package/src/components/pane-settings-dialog/tui.tsx +11 -9
  120. package/src/components/pane-settings-dialog/value.ts +8 -5
  121. package/src/components/pane-settings-dialog.test.tsx +235 -0
  122. package/src/components/pane-settings-dialog.tsx +24 -0
  123. package/src/components/pane-template-wizard.tsx +6 -2
  124. package/src/components/ticker/list-table-view.tsx +3 -2
  125. package/src/components/ui/button.tsx +3 -1
  126. package/src/components/ui/choice-dialog.tsx +6 -1
  127. package/src/components/ui/confirm-dialog.tsx +3 -2
  128. package/src/components/ui/fields.tsx +10 -1
  129. package/src/components/ui/frame.tsx +4 -1
  130. package/src/components/ui/index.ts +5 -0
  131. package/src/components/ui/inline-quick-add.tsx +104 -0
  132. package/src/components/ui/list-view.tsx +5 -4
  133. package/src/components/ui/loading.tsx +2 -1
  134. package/src/components/ui/multi-select/dialog.tsx +188 -44
  135. package/src/components/ui/popover.tsx +10 -0
  136. package/src/components/ui/shortcut-hint.tsx +2 -0
  137. package/src/components/ui/status.tsx +4 -3
  138. package/src/components/ui/tabs.tsx +19 -8
  139. package/src/components/ui/toggle.tsx +44 -3
  140. package/src/components/ui/ui.test.tsx +67 -5
  141. package/src/core/app-services.ts +4 -6
  142. package/src/core/state/session-persistence.ts +3 -2
  143. package/src/data/config/chart-settings.ts +712 -0
  144. package/src/data/config/layout.ts +24 -5
  145. package/src/data/config/store/index.test.ts +377 -14
  146. package/src/data/config/store/normalize.ts +79 -65
  147. package/src/data/fred-series.ts +137 -0
  148. package/src/i18n/index.test.ts +114 -0
  149. package/src/i18n/index.ts +147 -0
  150. package/src/i18n/ja.ts +1013 -0
  151. package/src/i18n/ko.ts +1011 -0
  152. package/src/i18n/languages.test.ts +31 -0
  153. package/src/i18n/languages.ts +72 -0
  154. package/src/i18n/zh-cn.ts +1013 -0
  155. package/src/i18n/zh-tw.ts +1013 -0
  156. package/src/plugins/builtin/account-management/footer.ts +3 -2
  157. package/src/plugins/builtin/account-management/form-components.tsx +3 -2
  158. package/src/plugins/builtin/account-management/keyboard.ts +6 -0
  159. package/src/plugins/builtin/account-management/model.ts +25 -21
  160. package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
  161. package/src/plugins/builtin/account-management/navigation.ts +24 -0
  162. package/src/plugins/builtin/account-management/pane.tsx +120 -85
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
  164. package/src/plugins/builtin/ai/agent-history.ts +139 -0
  165. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
  166. package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
  167. package/src/plugins/builtin/ai/index.test.ts +302 -0
  168. package/src/plugins/builtin/ai/index.tsx +374 -40
  169. package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
  170. package/src/plugins/builtin/ai/pane-settings.ts +319 -0
  171. package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
  172. package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
  173. package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
  174. package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
  175. package/src/plugins/builtin/ai/pi/host.ts +564 -0
  176. package/src/plugins/builtin/ai/pi/index.ts +5 -0
  177. package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
  178. package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
  179. package/src/plugins/builtin/ai/pi/providers.ts +68 -0
  180. package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
  181. package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
  182. package/src/plugins/builtin/ai/providers.test.ts +68 -0
  183. package/src/plugins/builtin/ai/providers.ts +163 -38
  184. package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
  185. package/src/plugins/builtin/ai/runner-selection.ts +193 -0
  186. package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
  187. package/src/plugins/builtin/ai/runner.test.ts +198 -0
  188. package/src/plugins/builtin/ai/runner.ts +276 -73
  189. package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
  190. package/src/plugins/builtin/ai/screener/contract.ts +14 -52
  191. package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
  192. package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
  193. package/src/plugins/builtin/ai/screener/footer.ts +82 -67
  194. package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
  195. package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
  196. package/src/plugins/builtin/ai/screener/model.ts +9 -3
  197. package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
  198. package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
  199. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
  200. package/src/plugins/builtin/ai/screener/runner.ts +41 -25
  201. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
  202. package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
  203. package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
  204. package/src/plugins/builtin/ai/workspace/model.ts +308 -0
  205. package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
  206. package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
  207. package/src/plugins/builtin/analytics/index.test.tsx +3 -3
  208. package/src/plugins/builtin/analytics/index.tsx +3 -8
  209. package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
  210. package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
  211. package/src/plugins/builtin/broker-manager/index.tsx +9 -11
  212. package/src/plugins/builtin/broker-manager/model.ts +11 -10
  213. package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
  214. package/src/plugins/builtin/broker-manager/table.ts +9 -8
  215. package/src/plugins/builtin/changelog/index.tsx +3 -8
  216. package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
  217. package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
  218. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
  219. package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
  220. package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
  221. package/src/plugins/builtin/chart-composer/index.tsx +228 -0
  222. package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
  223. package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
  224. package/src/plugins/builtin/chart-composer/presets.ts +759 -0
  225. package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
  226. package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
  227. package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
  228. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
  229. package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
  230. package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
  231. package/src/plugins/builtin/chart-composer/settings.ts +397 -0
  232. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
  233. package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
  234. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
  235. package/src/plugins/builtin/chat/channel-labels.ts +5 -4
  236. package/src/plugins/builtin/chat/channels.ts +12 -11
  237. package/src/plugins/builtin/chat/chat.test.tsx +32 -0
  238. package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
  239. package/src/plugins/builtin/chat/content/composer.tsx +9 -8
  240. package/src/plugins/builtin/chat/content/index.tsx +36 -1
  241. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
  242. package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
  243. package/src/plugins/builtin/chat/controller/utils.ts +10 -9
  244. package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
  245. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
  246. package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
  247. package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
  248. package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
  249. package/src/plugins/builtin/chat/pane-state.ts +30 -0
  250. package/src/plugins/builtin/chat/pane.tsx +27 -4
  251. package/src/plugins/builtin/chat/profile-popover.ts +62 -5
  252. package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
  253. package/src/plugins/builtin/chat/sidebar.tsx +107 -168
  254. package/src/plugins/builtin/chat/test-harness.tsx +6 -0
  255. package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
  256. package/src/plugins/builtin/cloud/plugin.tsx +150 -127
  257. package/src/plugins/builtin/composite-plugins.ts +71 -0
  258. package/src/plugins/builtin/correlation/index.test.tsx +4 -4
  259. package/src/plugins/builtin/correlation/index.tsx +3 -8
  260. package/src/plugins/builtin/earnings/index.tsx +37 -21
  261. package/src/plugins/builtin/earnings/model.test.ts +52 -0
  262. package/src/plugins/builtin/earnings/model.ts +32 -0
  263. package/src/plugins/builtin/econ/detail-view.tsx +17 -4
  264. package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
  265. package/src/plugins/builtin/econ/index.tsx +16 -20
  266. package/src/plugins/builtin/fear-greed/index.tsx +2 -8
  267. package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
  268. package/src/plugins/builtin/help/components.tsx +5 -4
  269. package/src/plugins/builtin/help/index.tsx +14 -17
  270. package/src/plugins/builtin/holders/index.tsx +2 -8
  271. package/src/plugins/builtin/insider/index.tsx +2 -7
  272. package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
  273. package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
  274. package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
  275. package/src/plugins/builtin/layout-manager/index.tsx +3 -7
  276. package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
  277. package/src/plugins/builtin/market-movers/index.tsx +3 -8
  278. package/src/plugins/builtin/news/index.tsx +12 -16
  279. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
  280. package/src/plugins/builtin/news/wire/index.tsx +83 -73
  281. package/src/plugins/builtin/options/index.tsx +2 -8
  282. package/src/plugins/builtin/plugin-module.test.ts +89 -0
  283. package/src/plugins/builtin/plugin-module.ts +116 -0
  284. package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
  285. package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
  286. package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
  287. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
  288. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
  289. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  290. package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
  291. package/src/plugins/builtin/research/index.tsx +2 -8
  292. package/src/plugins/builtin/sec/index.tsx +2 -8
  293. package/src/plugins/builtin/sectors/index.tsx +3 -8
  294. package/src/plugins/builtin/thirteenf/index.tsx +3 -8
  295. package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
  296. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
  297. package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
  298. package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
  299. package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
  300. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
  301. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
  302. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
  303. package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
  304. package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
  305. package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
  306. package/src/plugins/builtin/tv/channels.ts +27 -0
  307. package/src/plugins/builtin/tv/index.tsx +40 -0
  308. package/src/plugins/builtin/tv/pane.tsx +286 -0
  309. package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
  310. package/src/plugins/builtin/world-indices/index.tsx +3 -8
  311. package/src/plugins/builtin/yield-curve/index.tsx +8 -8
  312. package/src/plugins/catalog-backend.test.ts +21 -0
  313. package/src/plugins/catalog-backend.ts +42 -0
  314. package/src/plugins/catalog-ui.ts +6 -14
  315. package/src/plugins/ibkr/instance-selection.test.ts +0 -1
  316. package/src/plugins/ownership.ts +143 -0
  317. package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
  318. package/src/plugins/prediction-markets/chart.test.tsx +7 -5
  319. package/src/plugins/prediction-markets/chart.tsx +25 -7
  320. package/src/plugins/registry/index.test.ts +51 -0
  321. package/src/plugins/registry/index.ts +8 -0
  322. package/src/plugins/registry/pane-settings.ts +1 -1
  323. package/src/plugins/runtime/index.test.tsx +2 -2
  324. package/src/remote/app-host.tsx +16 -3
  325. package/src/remote/controller.test.ts +102 -1
  326. package/src/remote/controller.ts +122 -0
  327. package/src/remote/types.ts +11 -0
  328. package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
  329. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
  330. package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
  331. package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
  332. package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
  333. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
  334. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
  335. package/src/renderers/electrobun/bun/index.ts +5 -0
  336. package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
  337. package/src/renderers/electrobun/view/ai-host.ts +124 -19
  338. package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
  339. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
  340. package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
  341. package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
  342. package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
  343. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  344. package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
  345. package/src/renderers/electrobun/view/host/input.tsx +9 -19
  346. package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
  347. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  348. package/src/renderers/electrobun/view/host/style.ts +1 -1
  349. package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
  350. package/src/renderers/electrobun/view/main.tsx +10 -3
  351. package/src/renderers/electrobun/view/styles.css +17 -0
  352. package/src/renderers/electrobun/view/ui-host.tsx +7 -0
  353. package/src/renderers/opentui/chart-surface.test.tsx +52 -6
  354. package/src/renderers/opentui/chart-surface.tsx +95 -11
  355. package/src/renderers/opentui/dialog-host.tsx +36 -5
  356. package/src/renderers/opentui/host.tsx +39 -0
  357. package/src/renderers/opentui/start.tsx +30 -0
  358. package/src/renderers/opentui/ui-host.tsx +2 -0
  359. package/src/sources/provider-router/index.test.ts +43 -0
  360. package/src/sources/provider-router/index.ts +4 -0
  361. package/src/sources/provider-router/supplemental.ts +46 -0
  362. package/src/sources/sec-edgar.test.ts +203 -2
  363. package/src/sources/sec-edgar.ts +70 -15
  364. package/src/state/app/context/index.tsx +13 -1
  365. package/src/state/app/context/selector.test.tsx +62 -1
  366. package/src/state/session-persistence.test.ts +13 -2
  367. package/src/sync/controller.test.ts +190 -3
  368. package/src/sync/controller.ts +76 -45
  369. package/src/sync/core-contributors.test.ts +121 -0
  370. package/src/sync/core-contributors.ts +104 -27
  371. package/src/sync/react.ts +6 -8
  372. package/src/sync/types.ts +3 -0
  373. package/src/time-series/alignment.ts +158 -0
  374. package/src/time-series/economic.ts +49 -0
  375. package/src/time-series/field-catalog.ts +306 -0
  376. package/src/time-series/fundamentals.test.ts +505 -0
  377. package/src/time-series/fundamentals.ts +842 -0
  378. package/src/time-series/hooks.ts +45 -0
  379. package/src/time-series/index.ts +14 -0
  380. package/src/time-series/live-quotes.test.ts +219 -0
  381. package/src/time-series/live-quotes.ts +203 -0
  382. package/src/time-series/market-resolution.ts +53 -0
  383. package/src/time-series/market.ts +125 -0
  384. package/src/time-series/reporting.ts +98 -0
  385. package/src/time-series/resolve.test.ts +1281 -0
  386. package/src/time-series/resolve.ts +783 -0
  387. package/src/time-series/spec-studies.test.ts +400 -0
  388. package/src/time-series/spec.ts +487 -0
  389. package/src/time-series/studies.ts +563 -0
  390. package/src/time-series/transforms.test.ts +105 -0
  391. package/src/time-series/transforms.ts +152 -0
  392. package/src/time-series/types.ts +162 -0
  393. package/src/time-series/use-chart-resolution.ts +166 -0
  394. package/src/types/config.ts +9 -11
  395. package/src/types/desktop-window.ts +1 -0
  396. package/src/types/financials.ts +4 -0
  397. package/src/types/media.ts +17 -0
  398. package/src/types/plugin.ts +32 -1
  399. package/src/ui/context-menu.tsx +15 -3
  400. package/src/ui/dialog.tsx +14 -2
  401. package/src/ui/host.tsx +40 -1
  402. package/src/ui/index.tsx +9 -0
  403. package/src/utils/exchanges.test.ts +13 -1
  404. package/src/utils/exchanges.ts +10 -0
  405. package/src/utils/financial-statements.test.ts +138 -0
  406. package/src/utils/financial-statements.ts +115 -8
  407. package/src/utils/format.test.ts +11 -1
  408. package/src/utils/format.ts +7 -0
  409. package/src/version.ts +1 -1
  410. package/src/components/chart/bar-chart-renderer.test.ts +0 -233
  411. package/src/components/chart/bar-chart-renderer.ts +0 -445
  412. package/src/components/chart/comparison/data.test.ts +0 -159
  413. package/src/components/chart/comparison/data.ts +0 -302
  414. package/src/components/chart/comparison/renderer.test.ts +0 -187
  415. package/src/components/chart/comparison/renderer.ts +0 -282
  416. package/src/components/chart/comparison/scene.ts +0 -178
  417. package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
  418. package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
  419. package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
  420. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
  421. package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
  422. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
  423. package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
  424. package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
  425. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
  426. package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
  427. package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
  428. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
  429. package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
  430. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
  431. package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
  432. package/src/components/chart/comparison-stock-chart/types.ts +0 -22
  433. package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
  434. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
  435. package/src/components/chart/comparison-stock-chart.tsx +0 -24
  436. package/src/components/chart/core/axis-measure.ts +0 -33
  437. package/src/components/chart/core/controller.test.ts +0 -157
  438. package/src/components/chart/core/controller.ts +0 -349
  439. package/src/components/chart/core/indicator-selector.tsx +0 -75
  440. package/src/components/chart/core/keyboard.ts +0 -24
  441. package/src/components/chart/core/pane-settings.test.ts +0 -52
  442. package/src/components/chart/core/pane-settings.ts +0 -141
  443. package/src/components/chart/core/scroll.test.ts +0 -64
  444. package/src/components/chart/core/scroll.ts +0 -82
  445. package/src/components/chart/core/window-viewport.test.ts +0 -69
  446. package/src/components/chart/core/window-viewport.ts +0 -162
  447. package/src/components/chart/display-cursor.ts +0 -153
  448. package/src/components/chart/indicators/bands.test.ts +0 -36
  449. package/src/components/chart/indicators/bands.ts +0 -29
  450. package/src/components/chart/indicators/moving-averages.test.ts +0 -39
  451. package/src/components/chart/indicators/moving-averages.ts +0 -42
  452. package/src/components/chart/indicators/options.test.ts +0 -32
  453. package/src/components/chart/indicators/options.ts +0 -107
  454. package/src/components/chart/indicators/oscillators.test.ts +0 -58
  455. package/src/components/chart/indicators/oscillators.ts +0 -92
  456. package/src/components/chart/native/canvas-bitmaps.ts +0 -140
  457. package/src/components/chart/native/raster/comparison-chart.ts +0 -162
  458. package/src/components/chart/native/surface/hooks.ts +0 -144
  459. package/src/components/chart/native/surface/rendering.ts +0 -211
  460. package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
  461. package/src/components/chart/static/bar-chart-surface.tsx +0 -292
  462. package/src/components/chart/stock-chart/auto.ts +0 -216
  463. package/src/components/chart/stock-chart/axis-width.ts +0 -146
  464. package/src/components/chart/stock-chart/bitmaps.ts +0 -22
  465. package/src/components/chart/stock-chart/canvas.ts +0 -202
  466. package/src/components/chart/stock-chart/controls.tsx +0 -149
  467. package/src/components/chart/stock-chart/cursor.ts +0 -60
  468. package/src/components/chart/stock-chart/data-runtime.ts +0 -128
  469. package/src/components/chart/stock-chart/footer.test.ts +0 -65
  470. package/src/components/chart/stock-chart/footer.ts +0 -303
  471. package/src/components/chart/stock-chart/geometry.ts +0 -160
  472. package/src/components/chart/stock-chart/index.test.ts +0 -532
  473. package/src/components/chart/stock-chart/index.tsx +0 -29
  474. package/src/components/chart/stock-chart/indicators.ts +0 -226
  475. package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
  476. package/src/components/chart/stock-chart/interactions.ts +0 -300
  477. package/src/components/chart/stock-chart/keyboard.ts +0 -373
  478. package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
  479. package/src/components/chart/stock-chart/layout.tsx +0 -177
  480. package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
  481. package/src/components/chart/stock-chart/presentation.ts +0 -67
  482. package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
  483. package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
  484. package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
  485. package/src/components/chart/stock-chart/rendering/data.ts +0 -370
  486. package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
  487. package/src/components/chart/stock-chart/rendering/output.ts +0 -304
  488. package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
  489. package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
  490. package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
  491. package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
  492. package/src/components/chart/stock-chart/rendering/state.ts +0 -283
  493. package/src/components/chart/stock-chart/requests.ts +0 -337
  494. package/src/components/chart/stock-chart/resolution-support.ts +0 -96
  495. package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
  496. package/src/components/chart/stock-chart/resolved.tsx +0 -359
  497. package/src/components/chart/stock-chart/settings.ts +0 -81
  498. package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
  499. package/src/components/chart/stock-chart/types.ts +0 -25
  500. package/src/components/chart/stock-chart/view.tsx +0 -155
  501. package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
  502. package/src/components/chart/stock-chart/viewport/index.ts +0 -144
  503. package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
  504. package/src/components/chart/time-axis-label.tsx +0 -175
  505. package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
  506. package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
  507. package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
  508. package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
  509. package/src/plugins/builtin/econ/fred-cache.ts +0 -106
  510. package/src/plugins/builtin/plugin-groups.ts +0 -130
  511. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
  512. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
  513. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
  514. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
  515. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
  516. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
  517. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
  518. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
  519. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
@@ -1,15 +1,48 @@
1
1
  import { dirname, resolve } from "path";
2
2
  import { mkdir } from "fs/promises";
3
3
  import type { PaneRuntimeState } from "../../core/state/app/state";
4
- import type { OptionsChain, TickerFinancials } from "../../types/financials";
4
+ import { CHART_COMPOSER_PANE_ID } from "../../types/config";
5
+ import type { OptionsChain, PricePoint, TickerFinancials } from "../../types/financials";
5
6
  import type { TickerRecord } from "../../types/ticker";
6
7
  import { slugifyName } from "../../utils/slugify";
7
- import { renderDesktopPaneScreenshot, type DesktopPaneShotPayload } from "../desktop-pane-shot";
8
+ import {
9
+ renderDesktopPaneScreenshot,
10
+ type DesktopPaneShotPayload,
11
+ type DesktopPaneShotRenderResult,
12
+ } from "../desktop-pane-shot";
8
13
  import { optionPaneState } from "./options";
9
14
  import type { ResolvedPaneFunction } from "./resolver";
10
15
  import type { MarketContext } from "../types";
16
+ import { capabilityPluginState } from "./capabilities";
17
+ import type { RemoteUiNodeSnapshot } from "../../remote/types";
18
+ import {
19
+ graphRowsForFinancials,
20
+ limitGraphRowsBySymbol,
21
+ metricDef,
22
+ } from "../../time-series/reporting";
23
+ import {
24
+ buildFinancialTableModel,
25
+ formatFinancialHeader,
26
+ } from "../../plugins/builtin/ticker-detail/financials/model";
27
+ import type {
28
+ FundamentalPeriod,
29
+ GraphKind,
30
+ GraphMetricKey,
31
+ } from "../../time-series/reporting";
32
+ import type { TimeRange } from "../../components/chart/core/types";
33
+ import { appendLiveQuotePoint } from "../../components/chart/core/data";
34
+ import {
35
+ buildPresetDateWindow,
36
+ getVisibleWindowForDateRange,
37
+ subtractTimeRange,
38
+ } from "../../components/chart/core/date-window";
39
+ import { parseChartSpec } from "../../plugins/builtin/chart-composer/chart-spec";
40
+ import { publicTickerKey } from "../../utils/exchanges";
41
+ import { apiClient } from "../../api-client";
42
+ import type { FredSeriesCacheEntry } from "../../data/fred-series";
11
43
  import {
12
44
  collectShotSymbols,
45
+ clipPriceHistoryToRange,
13
46
  createFallbackTicker,
14
47
  fetchTickerFinancials,
15
48
  isFinancialAnalysisFunction,
@@ -20,6 +53,153 @@ const DESKTOP_CELL_WIDTH_PX = 8;
20
53
  const DESKTOP_CELL_HEIGHT_PX = 18;
21
54
  const OPTIONS_PANE_ID = "options";
22
55
 
56
+ async function collectShotFredSeries(
57
+ resolved: ResolvedPaneFunction,
58
+ ): Promise<Array<[string, FredSeriesCacheEntry]>> {
59
+ if (resolved.pane.id !== CHART_COMPOSER_PANE_ID) return [];
60
+ const spec = parseChartSpec(resolved.instance.settings?.chartSpec);
61
+ if (!spec) return [];
62
+ const seriesIds = [...new Set(spec.series.flatMap((series) => (
63
+ series.source.kind === "economic" ? [series.source.seriesId.trim().toUpperCase()] : []
64
+ )))];
65
+ const loaded = await Promise.all(seriesIds.map(async (seriesId) => {
66
+ try {
67
+ const data = await apiClient.getCloudFredSeries(seriesId, {
68
+ startDate: "1900-01-01",
69
+ sortOrder: "asc",
70
+ });
71
+ return [seriesId, { data, fetchedAt: Date.now(), stale: false }] as [string, FredSeriesCacheEntry];
72
+ } catch {
73
+ return null;
74
+ }
75
+ }));
76
+ return loaded.filter((entry): entry is [string, FredSeriesCacheEntry] => !!entry);
77
+ }
78
+
79
+ export interface PaneScreenshotExpectedSelection {
80
+ control: "metric" | "statement" | "period";
81
+ value?: string;
82
+ label?: string;
83
+ }
84
+
85
+ export interface PaneScreenshotChartPointEvidence {
86
+ date: string;
87
+ close: number;
88
+ }
89
+
90
+ export interface PaneScreenshotChartSeriesEvidence {
91
+ symbol: string;
92
+ pointCount: number;
93
+ first: PaneScreenshotChartPointEvidence | null;
94
+ last: PaneScreenshotChartPointEvidence | null;
95
+ projectionBaseValue?: number | null;
96
+ projectionLatestRawValue?: number | null;
97
+ projectionLatestValue?: number | null;
98
+ }
99
+
100
+ export interface PaneScreenshotExpectedChartEvidence {
101
+ kind: "stock-price" | "price-comparison" | "chart-composer";
102
+ symbols: string[];
103
+ rangePreset: string;
104
+ axisMode?: string;
105
+ resolution?: string;
106
+ sourceSeries?: PaneScreenshotChartSeriesEvidence[];
107
+ baseSeries?: Array<{
108
+ id: string;
109
+ sourceKind: "security" | "economic";
110
+ symbol?: string;
111
+ fieldId?: string;
112
+ economicSeriesId?: string;
113
+ style: string;
114
+ transform: string;
115
+ panelId: string;
116
+ visible: boolean;
117
+ }>;
118
+ }
119
+
120
+ export interface PaneScreenshotPriceComparisonEvidence {
121
+ kind: "price-comparison";
122
+ symbols: string[];
123
+ range: string;
124
+ series: Array<{
125
+ symbol: string;
126
+ base: { date: string; value: number };
127
+ latest: { date: string; value: number };
128
+ returnPercent: number;
129
+ }>;
130
+ }
131
+
132
+ export interface PaneScreenshotPriceSeriesEvidence {
133
+ kind: "price-series";
134
+ symbol: string;
135
+ range: string;
136
+ pointCount: number;
137
+ first: PaneScreenshotChartPointEvidence;
138
+ last: PaneScreenshotChartPointEvidence;
139
+ }
140
+
141
+ export interface PaneScreenshotFundamentalSeriesEvidence {
142
+ kind: "fundamental-series";
143
+ metric: string;
144
+ period: FundamentalPeriod;
145
+ series: Array<{
146
+ symbol: string;
147
+ rows: Array<{ date: string; value: number }>;
148
+ }>;
149
+ }
150
+
151
+ export interface PaneScreenshotFinancialStatementEvidence {
152
+ kind: "financial-statement";
153
+ symbol: string;
154
+ statement: string;
155
+ period: FundamentalPeriod;
156
+ latest: {
157
+ date: string;
158
+ metrics: Array<{
159
+ id: string;
160
+ key: string;
161
+ label: string;
162
+ value: number;
163
+ }>;
164
+ };
165
+ }
166
+
167
+ export type PaneScreenshotDataEvidence =
168
+ | PaneScreenshotPriceSeriesEvidence
169
+ | PaneScreenshotPriceComparisonEvidence
170
+ | PaneScreenshotFundamentalSeriesEvidence
171
+ | PaneScreenshotFinancialStatementEvidence;
172
+
173
+ export interface PaneScreenshotResult {
174
+ kind: "pane-screenshot";
175
+ target: string;
176
+ capability: {
177
+ id: string;
178
+ botSafe: boolean;
179
+ outputKind: string;
180
+ reportReadiness: ResolvedPaneFunction["capability"]["reportReadiness"];
181
+ screenshotReadiness: ResolvedPaneFunction["capability"]["screenshotReadiness"];
182
+ };
183
+ symbols: string[];
184
+ options: Record<string, string | number | boolean>;
185
+ rowCount: number;
186
+ empty: boolean;
187
+ complete: boolean;
188
+ unavailableSymbols: string[];
189
+ semanticMismatch: boolean;
190
+ usable: boolean;
191
+ dataEvidence: PaneScreenshotDataEvidence | null;
192
+ outputPath: string;
193
+ render: DesktopPaneShotRenderResult & {
194
+ expectedText: string[];
195
+ missingExpectedText: string[];
196
+ expectedSelections: PaneScreenshotExpectedSelection[];
197
+ missingExpectedSelections: PaneScreenshotExpectedSelection[];
198
+ expectedChart: PaneScreenshotExpectedChartEvidence | null;
199
+ chartEvidenceMismatches: string[];
200
+ };
201
+ }
202
+
23
203
  export function defaultScreenshotPath(resolved: ResolvedPaneFunction, rawArg: string): string {
24
204
  const suffix = slugifyName([resolved.token, rawArg].filter(Boolean).join("-"), "pane");
25
205
  return resolve(process.cwd(), `gloomberb-${suffix}.png`);
@@ -35,7 +215,14 @@ async function buildDesktopShotPayload(
35
215
  ): Promise<DesktopPaneShotPayload> {
36
216
  const widthCells = Math.max(1, Math.round(widthPx / DESKTOP_CELL_WIDTH_PX));
37
217
  const heightCells = Math.max(1, Math.round(heightPx / DESKTOP_CELL_HEIGHT_PX));
38
- const initialPaneState = optionPaneState(options);
218
+ const initialPaneState = optionPaneState(resolved.options);
219
+ const pluginState = capabilityPluginState(resolved.capability, resolved.options);
220
+ if (Object.keys(pluginState).length > 0) {
221
+ initialPaneState.pluginState = {
222
+ ...(initialPaneState.pluginState ?? {}),
223
+ ...pluginState,
224
+ };
225
+ }
39
226
  if (isFinancialAnalysisFunction(resolved) && !initialPaneState.activeTabId) {
40
227
  initialPaneState.activeTabId = "financials";
41
228
  }
@@ -72,18 +259,34 @@ async function buildDesktopShotPayload(
72
259
  const tickers: TickerRecord[] = [];
73
260
  const financials: Array<[string, TickerFinancials]> = [];
74
261
  const optionsChains: Array<[string, OptionsChain]> = [];
262
+ const fredSeries = await collectShotFredSeries(resolved);
75
263
  const includeOptionsChains = resolved.pane.id === OPTIONS_PANE_ID || resolved.template?.paneId === OPTIONS_PANE_ID;
76
264
  for (const symbol of collectShotSymbols(resolved, rawArg)) {
77
265
  const entry = await fetchTickerFinancials(context, symbol);
78
- const data = await withShotPriceHistory(context, symbol, entry.tickerFile, entry.financials);
266
+ const requestedRange = shotPriceHistoryRange(resolved);
267
+ let data = entry.financials;
268
+ if (requestedRange) {
269
+ const exchange = entry.instrument.exchange
270
+ ?? entry.tickerFile?.metadata.exchange
271
+ ?? data.quote?.listingExchangeName
272
+ ?? data.quote?.exchangeName
273
+ ?? "";
274
+ try {
275
+ const priceHistory = await context.dataProvider.getPriceHistory(entry.instrument.symbol, exchange, requestedRange);
276
+ data = { ...data, priceHistory: clipPriceHistoryToRange(priceHistory, requestedRange) };
277
+ } catch {
278
+ data = await withShotPriceHistory(context, symbol, entry.tickerFile, data);
279
+ }
280
+ }
79
281
  tickers.push(entry.tickerFile ?? createFallbackTicker(symbol, data, context));
80
282
  financials.push([symbol, data]);
81
283
  if (includeOptionsChains && context.dataProvider.getOptionsChain) {
82
- const exchange = entry.tickerFile?.metadata.exchange
284
+ const exchange = entry.instrument.exchange
285
+ ?? entry.tickerFile?.metadata.exchange
83
286
  ?? data.quote?.listingExchangeName
84
287
  ?? data.quote?.exchangeName
85
288
  ?? "";
86
- const chain = await context.dataProvider.getOptionsChain(symbol, exchange);
289
+ const chain = await context.dataProvider.getOptionsChain(entry.instrument.symbol, exchange);
87
290
  optionsChains.push([symbol, chain]);
88
291
  }
89
292
  }
@@ -98,10 +301,32 @@ async function buildDesktopShotPayload(
98
301
  tickers,
99
302
  financials,
100
303
  optionsChains,
304
+ fredSeries,
101
305
  paneState,
102
306
  };
103
307
  }
104
308
 
309
+ export function shotPriceHistoryRange(resolved: ResolvedPaneFunction): TimeRange | null {
310
+ switch (resolved.capability.id) {
311
+ case "chart-composer":
312
+ return parseChartSpec(resolved.instance.settings?.chartSpec)?.viewport.range ?? null;
313
+ case "valuation-series":
314
+ return "ALL";
315
+ case "price-chart":
316
+ return (resolved.options.rangePreset ?? "5Y") as TimeRange;
317
+ case "price-comparison":
318
+ case "return-correlation":
319
+ return (resolved.options.rangePreset ?? "1Y") as TimeRange;
320
+ case "intraday-price-chart":
321
+ return "1D";
322
+ case "historical-prices":
323
+ case "security-relationship":
324
+ return (resolved.options.range ?? "1Y") as TimeRange;
325
+ default:
326
+ return null;
327
+ }
328
+ }
329
+
105
330
  export async function renderDesktopShot({
106
331
  resolved,
107
332
  context,
@@ -118,8 +343,727 @@ export async function renderDesktopShot({
118
343
  width: number;
119
344
  height: number;
120
345
  options: Record<string, string | true>;
121
- }): Promise<void> {
346
+ }): Promise<PaneScreenshotResult> {
122
347
  await mkdir(dirname(outputPath), { recursive: true });
123
348
  const payload = await buildDesktopShotPayload(resolved, context, rawArg, options, width, height);
124
- await renderDesktopPaneScreenshot(payload, outputPath);
349
+ const render = await renderDesktopPaneScreenshot(payload, outputPath);
350
+ const symbols = payload.financials.map(([symbol]) => symbol);
351
+ const rowCount = shotSemanticRowCount(resolved, payload, render.semanticUi);
352
+ const unavailableSymbols = shotUnavailableSymbols(resolved, payload, render.semanticUi);
353
+ const complete = unavailableSymbols.length === 0;
354
+ const expectedText = shotExpectedText(resolved, symbols, payload);
355
+ const normalizedVisibleText = render.visibleText.toLowerCase();
356
+ const missingExpectedText = expectedText.filter((value) => !normalizedVisibleText.includes(value.toLowerCase()));
357
+ const expectedSelections = shotExpectedSelections(resolved);
358
+ const missingExpectedSelections = missingActiveTabSelections(
359
+ render.semanticUi,
360
+ expectedSelections,
361
+ );
362
+ const expectedChart = shotExpectedChart(resolved, payload);
363
+ const dataEvidence = shotDataEvidenceFor(resolved, payload);
364
+ const chartEvidenceMismatches = expectedChart
365
+ ? chartEvidenceMismatchesFor(render.semanticUi, expectedChart)
366
+ : [];
367
+ const semanticMismatch = missingExpectedText.length > 0
368
+ || missingExpectedSelections.length > 0
369
+ || chartEvidenceMismatches.length > 0;
370
+ const empty = render.emptyStateDetected || rowCount === 0 || semanticMismatch;
371
+ const usable = resolved.capability.botSafe
372
+ && resolved.capability.screenshotReadiness === "ready"
373
+ && !empty
374
+ && complete
375
+ && (!requiresStructuredDataEvidence(resolved) || dataEvidence !== null);
376
+ return {
377
+ kind: "pane-screenshot",
378
+ target: resolved.token,
379
+ capability: {
380
+ id: resolved.capability.id,
381
+ botSafe: resolved.capability.botSafe,
382
+ outputKind: resolved.capability.outputKind,
383
+ reportReadiness: resolved.capability.reportReadiness,
384
+ screenshotReadiness: resolved.capability.screenshotReadiness,
385
+ },
386
+ symbols,
387
+ options: resolved.options,
388
+ rowCount,
389
+ empty,
390
+ complete,
391
+ unavailableSymbols,
392
+ semanticMismatch,
393
+ usable,
394
+ dataEvidence,
395
+ outputPath,
396
+ render: {
397
+ ...render,
398
+ expectedText,
399
+ missingExpectedText,
400
+ expectedSelections,
401
+ missingExpectedSelections,
402
+ expectedChart,
403
+ chartEvidenceMismatches,
404
+ },
405
+ };
406
+ }
407
+
408
+ function requiresStructuredDataEvidence(resolved: ResolvedPaneFunction): boolean {
409
+ return ["price-chart", "price-comparison", "fundamental-series", "financial-statements"]
410
+ .includes(resolved.capability.id);
411
+ }
412
+
413
+ const STATEMENT_EVIDENCE_KEYS: Record<string, ReadonlySet<string>> = {
414
+ income: new Set([
415
+ "totalRevenue",
416
+ "grossProfit",
417
+ "operatingIncome",
418
+ "netIncome",
419
+ "netIncomeCommonStockholders",
420
+ "eps",
421
+ "basicEps",
422
+ ]),
423
+ cashflow: new Set([
424
+ "operatingCashFlow",
425
+ "capitalExpenditure",
426
+ "freeCashFlow",
427
+ ]),
428
+ balance: new Set([
429
+ "totalAssets",
430
+ "totalLiabilities",
431
+ "cashAndCashEquivalents",
432
+ "totalDebt",
433
+ "totalEquity",
434
+ "commonStockEquity",
435
+ ]),
436
+ };
437
+
438
+ export function shotDataEvidenceFor(
439
+ resolved: ResolvedPaneFunction,
440
+ payload: DesktopPaneShotPayload,
441
+ ): PaneScreenshotDataEvidence | null {
442
+ if (resolved.capability.id === "price-chart") {
443
+ const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
444
+ const [symbol, financials] = payload.financials[0] ?? [];
445
+ if (!symbol || !financials) return null;
446
+ const { evidence } = normalizeChartSeries(symbol, financials, range);
447
+ if (!evidence.first || !evidence.last || evidence.pointCount <= 0) return null;
448
+ return {
449
+ kind: "price-series",
450
+ symbol,
451
+ range,
452
+ pointCount: evidence.pointCount,
453
+ first: evidence.first,
454
+ last: evidence.last,
455
+ };
456
+ }
457
+
458
+ if (resolved.capability.id === "price-comparison") {
459
+ const range = String(resolved.options.rangePreset ?? "1Y") as TimeRange;
460
+ const normalizedSeries = payload.financials.map(([symbol, financials]) => ({
461
+ symbol,
462
+ points: normalizeChartSeries(symbol, financials, range).points,
463
+ }));
464
+ const latestTimestamp = Math.max(
465
+ ...normalizedSeries.flatMap(({ points }) => points.map(({ date }) => date.getTime())),
466
+ );
467
+ if (!Number.isFinite(latestTimestamp)) return null;
468
+ const projectionStart = subtractTimeRange(new Date(latestTimestamp), range).getTime();
469
+ const series = normalizedSeries.flatMap(({ symbol, points }) => {
470
+ const projectionPoints = points.filter(({ date }) => date.getTime() >= projectionStart);
471
+ const base = projectionPoints[0];
472
+ const latest = projectionPoints.at(-1);
473
+ if (!base || !latest || !Number.isFinite(base.close) || !Number.isFinite(latest.close) || base.close === 0) {
474
+ return [];
475
+ }
476
+ return [{
477
+ symbol,
478
+ base: { date: base.date.toISOString(), value: base.close },
479
+ latest: { date: latest.date.toISOString(), value: latest.close },
480
+ returnPercent: ((latest.close - base.close) / base.close) * 100,
481
+ }];
482
+ });
483
+ if (series.length !== normalizedSeries.length) return null;
484
+ return {
485
+ kind: "price-comparison",
486
+ symbols: series.map(({ symbol }) => symbol),
487
+ range,
488
+ series,
489
+ };
490
+ }
491
+
492
+ if (resolved.capability.id === "fundamental-series") {
493
+ const metric = resolved.options.metric as GraphMetricKey;
494
+ const period = resolved.options.period as FundamentalPeriod;
495
+ const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
496
+ const evidencePeriodCount = Math.min(
497
+ periodCount ?? (period === "annual" ? 6 : 8),
498
+ period === "annual" ? 6 : 8,
499
+ );
500
+ const series = payload.financials.map(([symbol, financials]) => ({
501
+ symbol,
502
+ rows: limitGraphRowsBySymbol(
503
+ graphRowsForFinancials(financials, "fundamental", metric, period, symbol),
504
+ evidencePeriodCount,
505
+ )
506
+ .sort((left, right) => left.date.localeCompare(right.date))
507
+ .map(({ date, value }) => ({ date, value })),
508
+ }));
509
+ if (series.some(({ rows }) => rows.length === 0)) return null;
510
+ return { kind: "fundamental-series", metric, period, series };
511
+ }
512
+
513
+ if (resolved.capability.id === "financial-statements") {
514
+ const [symbol, financials] = payload.financials[0] ?? [];
515
+ if (!symbol || !financials) return null;
516
+ const table = buildFinancialTableModel(financials, {
517
+ period: resolved.options.period as FundamentalPeriod,
518
+ statement: String(resolved.options.statement),
519
+ });
520
+ if (!table || table.statements.length === 0) return null;
521
+ const allowedKeys = STATEMENT_EVIDENCE_KEYS[table.subTab.key] ?? new Set<string>();
522
+ const latestPopulated = table.statements.flatMap((statement, statementIndex) => {
523
+ const seenKeys = new Set<string>();
524
+ const metrics = table.rows.flatMap((row) => {
525
+ const key = String(row.key ?? row.summaryKey ?? "");
526
+ const value = row.cells[statementIndex]?.value;
527
+ if (!allowedKeys.has(key) || seenKeys.has(key) || typeof value !== "number" || !Number.isFinite(value)) {
528
+ return [];
529
+ }
530
+ seenKeys.add(key);
531
+ return [{
532
+ id: row.id,
533
+ key,
534
+ label: row.unitLabel,
535
+ value,
536
+ }];
537
+ });
538
+ return metrics.length > 0 ? [{ statement, metrics }] : [];
539
+ })[0];
540
+ if (!latestPopulated) return null;
541
+ return {
542
+ kind: "financial-statement",
543
+ symbol,
544
+ statement: table.subTab.key,
545
+ period: table.period,
546
+ latest: {
547
+ date: latestPopulated.statement.date,
548
+ metrics: latestPopulated.metrics,
549
+ },
550
+ };
551
+ }
552
+
553
+ return null;
554
+ }
555
+
556
+ function normalizeChartSeries(
557
+ symbol: string,
558
+ financials: TickerFinancials,
559
+ range: TimeRange,
560
+ ): {
561
+ evidence: PaneScreenshotChartSeriesEvidence;
562
+ points: Array<{ date: Date; close: number }>;
563
+ } {
564
+ const sorted = clipPriceHistoryToRange(financials.priceHistory, range)
565
+ .flatMap((point) => {
566
+ const date = new Date(point.date);
567
+ return Number.isFinite(date.getTime()) ? [{ ...point, date }] : [];
568
+ })
569
+ .sort((left, right) => left.date.getTime() - right.date.getTime());
570
+ const points = appendLiveQuotePoint(sorted, financials.quote)
571
+ .slice()
572
+ .sort((left, right) => new Date(left.date).getTime() - new Date(right.date).getTime());
573
+ return {
574
+ points,
575
+ evidence: chartSeriesEvidence(symbol, points),
576
+ };
577
+ }
578
+
579
+ function chartSeriesEvidence(
580
+ symbol: string,
581
+ points: PricePoint[],
582
+ ): PaneScreenshotChartSeriesEvidence {
583
+ const pointEvidence = (
584
+ point: typeof points[number] | undefined,
585
+ ): PaneScreenshotChartPointEvidence | null => (
586
+ point && Number.isFinite(point.date.getTime())
587
+ ? { date: point.date.toISOString(), close: point.close }
588
+ : null
589
+ );
590
+ return {
591
+ symbol,
592
+ pointCount: points.length,
593
+ first: pointEvidence(points[0]),
594
+ last: pointEvidence(points.at(-1)),
595
+ };
596
+ }
597
+
598
+ export function chartSeriesEvidenceWithinRange(
599
+ symbol: string,
600
+ points: PricePoint[],
601
+ range: TimeRange,
602
+ ): PaneScreenshotChartSeriesEvidence {
603
+ const dateWindow = buildPresetDateWindow(points.map(({ date }) => date), range);
604
+ const visiblePoints = getVisibleWindowForDateRange(points, dateWindow, 0).points;
605
+ return chartSeriesEvidence(symbol, visiblePoints);
606
+ }
607
+
608
+ function shotExpectedChart(
609
+ resolved: ResolvedPaneFunction,
610
+ payload: DesktopPaneShotPayload,
611
+ ): PaneScreenshotExpectedChartEvidence | null {
612
+ if (resolved.pane.id === CHART_COMPOSER_PANE_ID) {
613
+ const spec = parseChartSpec(resolved.instance.settings?.chartSpec);
614
+ if (!spec) return null;
615
+ return {
616
+ kind: "chart-composer",
617
+ symbols: [...new Set(spec.series.flatMap((series) => (
618
+ series.source.kind === "security"
619
+ ? [publicTickerKey(series.source.instrument.symbol, series.source.instrument.exchange)]
620
+ : []
621
+ )))],
622
+ rangePreset: spec.viewport.range,
623
+ resolution: spec.viewport.resolution,
624
+ baseSeries: spec.series.map((series) => ({
625
+ id: series.id,
626
+ sourceKind: series.source.kind,
627
+ ...(series.source.kind === "security"
628
+ ? {
629
+ symbol: publicTickerKey(series.source.instrument.symbol, series.source.instrument.exchange),
630
+ fieldId: series.source.fieldId,
631
+ }
632
+ : { economicSeriesId: series.source.seriesId }),
633
+ style: series.style,
634
+ transform: series.transform,
635
+ panelId: series.panelId,
636
+ visible: series.visible !== false,
637
+ })),
638
+ };
639
+ }
640
+ if (resolved.capability.id === "price-chart") {
641
+ const range = String(resolved.options.rangePreset ?? "5Y") as TimeRange;
642
+ const sourceSeries = payload.financials.slice(0, 1).map(([symbol, financials]) => {
643
+ const { points } = normalizeChartSeries(symbol, financials, range);
644
+ return chartSeriesEvidenceWithinRange(symbol, points, range);
645
+ });
646
+ return {
647
+ kind: "stock-price",
648
+ symbols: sourceSeries.map(({ symbol }) => symbol),
649
+ rangePreset: range,
650
+ axisMode: "price",
651
+ sourceSeries,
652
+ };
653
+ }
654
+ if (resolved.capability.id === "price-comparison") {
655
+ const range = String(resolved.options.rangePreset ?? "1Y") as TimeRange;
656
+ const normalizedSeries = payload.financials.map(([symbol, financials]) => (
657
+ normalizeChartSeries(symbol, financials, range)
658
+ ));
659
+ const latestTimestamp = Math.max(
660
+ ...normalizedSeries.flatMap(({ points }) => points.map(({ date }) => date.getTime())),
661
+ );
662
+ const projectionStart = Number.isFinite(latestTimestamp)
663
+ ? subtractTimeRange(new Date(latestTimestamp), range).getTime()
664
+ : Number.NaN;
665
+ const axisMode = String(resolved.options.axisMode ?? "percent");
666
+ const sourceSeries = normalizedSeries.map(({ evidence, points }) => {
667
+ const projectionPoints = points.filter(({ date }) => date.getTime() >= projectionStart);
668
+ const baseValue = projectionPoints[0]?.close ?? null;
669
+ const latestRawValue = projectionPoints.at(-1)?.close ?? null;
670
+ const latestValue = baseValue == null || latestRawValue == null
671
+ ? null
672
+ : axisMode === "percent"
673
+ ? ((latestRawValue - baseValue) / baseValue) * 100
674
+ : latestRawValue;
675
+ return {
676
+ ...evidence,
677
+ projectionBaseValue: baseValue,
678
+ projectionLatestRawValue: latestRawValue,
679
+ projectionLatestValue: latestValue,
680
+ };
681
+ });
682
+ return {
683
+ kind: "price-comparison",
684
+ symbols: sourceSeries.map(({ symbol }) => symbol),
685
+ rangePreset: range,
686
+ axisMode,
687
+ resolution: String(resolved.options.chartResolution ?? "1d"),
688
+ sourceSeries,
689
+ };
690
+ }
691
+ return null;
692
+ }
693
+
694
+ function isRecord(value: unknown): value is Record<string, unknown> {
695
+ return !!value && typeof value === "object" && !Array.isArray(value);
696
+ }
697
+
698
+ function readStringArray(value: unknown): string[] | null {
699
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string")
700
+ ? value
701
+ : null;
702
+ }
703
+
704
+ function closeEnough(actual: unknown, expected: number): boolean {
705
+ return typeof actual === "number"
706
+ && Number.isFinite(actual)
707
+ && Math.abs(actual - expected) <= Math.max(1e-8, Math.abs(expected) * 1e-8);
708
+ }
709
+
710
+ function pointEvidenceMatches(
711
+ actual: unknown,
712
+ expected: PaneScreenshotChartPointEvidence | null,
713
+ ): boolean {
714
+ if (expected === null) return actual === null;
715
+ return isRecord(actual)
716
+ && actual.date === expected.date
717
+ && closeEnough(actual.close, expected.close);
718
+ }
719
+
720
+ function sourceSeriesMismatches(
721
+ actual: unknown,
722
+ expected: PaneScreenshotChartSeriesEvidence[],
723
+ ): string[] {
724
+ if (!Array.isArray(actual)) return ["chart source-series evidence is missing"];
725
+ if (actual.length !== expected.length) return ["chart source-series count does not match"];
726
+ const mismatches: string[] = [];
727
+ expected.forEach((expectedSeries, index) => {
728
+ const actualSeries = actual[index];
729
+ if (!isRecord(actualSeries) || actualSeries.symbol !== expectedSeries.symbol) {
730
+ mismatches.push(`chart source symbol ${index + 1} does not match`);
731
+ return;
732
+ }
733
+ if (actualSeries.pointCount !== expectedSeries.pointCount) {
734
+ mismatches.push(`${expectedSeries.symbol} chart source point count does not match`);
735
+ }
736
+ if (!pointEvidenceMatches(actualSeries.first, expectedSeries.first)) {
737
+ mismatches.push(`${expectedSeries.symbol} chart source first value does not match`);
738
+ }
739
+ if (!pointEvidenceMatches(actualSeries.last, expectedSeries.last)) {
740
+ mismatches.push(`${expectedSeries.symbol} chart source last value does not match`);
741
+ }
742
+ });
743
+ return mismatches;
744
+ }
745
+
746
+ export function chartEvidenceMismatchesFor(
747
+ semanticUi: RemoteUiNodeSnapshot[],
748
+ expected: PaneScreenshotExpectedChartEvidence,
749
+ ): string[] {
750
+ const node = semanticUi.find((candidate) => (
751
+ candidate.role === "chart-data"
752
+ && candidate.metadata?.kind === expected.kind
753
+ ));
754
+ const metadata = node?.metadata;
755
+ if (!metadata) return ["rendered chart-data semantic evidence is missing"];
756
+
757
+ const mismatches: string[] = [];
758
+ const symbols = readStringArray(metadata.symbols);
759
+ if (!symbols || symbols.join("\0") !== expected.symbols.join("\0")) {
760
+ mismatches.push("rendered chart symbols do not match");
761
+ }
762
+ if (metadata.rangePreset !== expected.rangePreset) {
763
+ mismatches.push("rendered chart range does not match");
764
+ }
765
+
766
+ if (expected.kind === "chart-composer") {
767
+ if (expected.resolution != null && metadata.resolution !== expected.resolution) {
768
+ mismatches.push("rendered chart resolution does not match");
769
+ }
770
+ const actualSeries = Array.isArray(metadata.baseSeries) ? metadata.baseSeries : [];
771
+ const expectedSeries = expected.baseSeries ?? [];
772
+ if (actualSeries.length !== expectedSeries.length) {
773
+ mismatches.push("rendered chart base-series count does not match");
774
+ } else {
775
+ expectedSeries.forEach((series, index) => {
776
+ const actual = actualSeries[index];
777
+ if (!isRecord(actual)
778
+ || actual.id !== series.id
779
+ || actual.sourceKind !== series.sourceKind
780
+ || actual.symbol !== series.symbol
781
+ || actual.fieldId !== series.fieldId
782
+ || actual.economicSeriesId !== series.economicSeriesId
783
+ || actual.style !== series.style
784
+ || actual.transform !== series.transform
785
+ || actual.panelId !== series.panelId
786
+ || actual.visible !== series.visible) {
787
+ mismatches.push(`rendered chart series ${series.id} does not match`);
788
+ return;
789
+ }
790
+ if (series.visible && (typeof actual.pointCount !== "number" || actual.pointCount <= 0)) {
791
+ mismatches.push(`rendered chart series ${series.id} is empty`);
792
+ }
793
+ });
794
+ }
795
+ if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
796
+ mismatches.push("rendered chart projection is empty");
797
+ }
798
+ return mismatches;
799
+ }
800
+
801
+ if (expected.kind === "stock-price") {
802
+ if (metadata.axisMode !== expected.axisMode) {
803
+ mismatches.push("rendered chart axis mode does not match");
804
+ }
805
+ const expectedSeries = expected.sourceSeries?.[0];
806
+ if (!expectedSeries) return [...mismatches, "expected stock chart source evidence is missing"];
807
+ mismatches.push(...sourceSeriesMismatches([{
808
+ symbol: symbols?.[0],
809
+ pointCount: metadata.sourcePointCount,
810
+ first: metadata.sourceFirst,
811
+ last: metadata.sourceLast,
812
+ }], expected.sourceSeries ?? []));
813
+ if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
814
+ mismatches.push("rendered stock chart projection is empty");
815
+ }
816
+ return mismatches;
817
+ }
818
+
819
+ if (metadata.requestedAxisMode !== expected.axisMode || metadata.effectiveAxisMode !== expected.axisMode) {
820
+ mismatches.push("rendered comparison axis mode does not match");
821
+ }
822
+ if (
823
+ expected.resolution != null
824
+ && (
825
+ metadata.selectedResolution !== expected.resolution
826
+ || metadata.effectiveResolution !== expected.resolution
827
+ )
828
+ ) {
829
+ mismatches.push("rendered comparison resolution does not match");
830
+ }
831
+ const expectedSourceSeries = expected.sourceSeries ?? [];
832
+ mismatches.push(...sourceSeriesMismatches(metadata.sourceSeries, expectedSourceSeries));
833
+
834
+ const projectionSeries = Array.isArray(metadata.projectionSeries)
835
+ ? metadata.projectionSeries
836
+ : [];
837
+ if (projectionSeries.length !== expectedSourceSeries.length) {
838
+ mismatches.push("rendered comparison projection-series count does not match");
839
+ } else {
840
+ expectedSourceSeries.forEach((expectedSeries, index) => {
841
+ const actualSeries = projectionSeries[index];
842
+ if (!isRecord(actualSeries) || actualSeries.symbol !== expectedSeries.symbol) {
843
+ mismatches.push(`${expectedSeries.symbol} comparison projection is missing`);
844
+ return;
845
+ }
846
+ const baseValue = expectedSeries.projectionBaseValue;
847
+ const latestRawValue = expectedSeries.projectionLatestRawValue;
848
+ if (baseValue == null || latestRawValue == null) {
849
+ mismatches.push(`${expectedSeries.symbol} expected comparison values are empty`);
850
+ return;
851
+ }
852
+ const latestValue = expectedSeries.projectionLatestValue;
853
+ if (!closeEnough(actualSeries.baseValue, baseValue)) {
854
+ mismatches.push(`${expectedSeries.symbol} comparison base value does not match`);
855
+ }
856
+ if (!closeEnough(actualSeries.latestRawValue, latestRawValue)) {
857
+ mismatches.push(`${expectedSeries.symbol} comparison latest value does not match`);
858
+ }
859
+ if (latestValue == null || !closeEnough(actualSeries.latestValue, latestValue)) {
860
+ mismatches.push(`${expectedSeries.symbol} comparison transformed value does not match`);
861
+ }
862
+ if (actualSeries.pointCount !== metadata.projectedPointCount) {
863
+ mismatches.push(`${expectedSeries.symbol} comparison projection point count does not match`);
864
+ }
865
+ });
866
+ }
867
+ if (typeof metadata.projectedPointCount !== "number" || metadata.projectedPointCount <= 0) {
868
+ mismatches.push("rendered comparison projection is empty");
869
+ }
870
+ return mismatches;
871
+ }
872
+
873
+ export function shotUnavailableSymbols(
874
+ resolved: ResolvedPaneFunction,
875
+ payload: DesktopPaneShotPayload,
876
+ semanticUi: RemoteUiNodeSnapshot[] = [],
877
+ ): string[] {
878
+ if (resolved.capability.id === "chart-composer") {
879
+ const metadata = semanticUi.find((node) => (
880
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
881
+ ))?.metadata;
882
+ const baseSeries = Array.isArray(metadata?.baseSeries) ? metadata.baseSeries : [];
883
+ return baseSeries.flatMap((entry) => {
884
+ if (!isRecord(entry) || (typeof entry.pointCount === "number" && entry.pointCount > 0)) return [];
885
+ if (entry.sourceKind === "security" && typeof entry.symbol === "string") return [entry.symbol];
886
+ if (entry.sourceKind === "economic" && typeof entry.economicSeriesId === "string") {
887
+ return [`FRED:${entry.economicSeriesId}`];
888
+ }
889
+ return [];
890
+ });
891
+ }
892
+ const graphKind = shotGraphKind(resolved);
893
+ if (graphKind) {
894
+ const metric = resolved.options.metric as GraphMetricKey;
895
+ const period = resolved.options.period as FundamentalPeriod;
896
+ const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
897
+ return payload.financials.flatMap(([symbol, financials]) => (
898
+ limitGraphRowsBySymbol(
899
+ graphRowsForFinancials(financials, graphKind, metric, period, symbol),
900
+ periodCount,
901
+ ).length === 0 ? [symbol] : []
902
+ ));
903
+ }
904
+ if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
905
+ return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 0 ? [] : [symbol]);
906
+ }
907
+ if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
908
+ return payload.financials.flatMap(([symbol, financials]) => financials.priceHistory.length > 1 ? [] : [symbol]);
909
+ }
910
+ if (resolved.capability.id === "financial-statements") {
911
+ return payload.financials.flatMap(([symbol, financials]) => {
912
+ const count = resolved.options.period === "quarterly"
913
+ ? financials.quarterlyStatements.length
914
+ : financials.annualStatements.length;
915
+ return count > 0 ? [] : [symbol];
916
+ });
917
+ }
918
+ return payload.financials.flatMap(([symbol, financials]) => financials.quote ? [] : [symbol]);
919
+ }
920
+
921
+ function shotGraphKind(resolved: ResolvedPaneFunction): GraphKind | null {
922
+ if (resolved.capability.id === "fundamental-series") return "fundamental";
923
+ if (resolved.capability.id === "valuation-series") return "valuation";
924
+ return null;
925
+ }
926
+
927
+ export function shotSemanticRowCount(
928
+ resolved: ResolvedPaneFunction,
929
+ payload: DesktopPaneShotPayload,
930
+ semanticUi: RemoteUiNodeSnapshot[] = [],
931
+ ): number {
932
+ if (resolved.capability.id === "chart-composer") {
933
+ const metadata = semanticUi.find((node) => (
934
+ node.role === "chart-data" && node.metadata?.kind === "chart-composer"
935
+ ))?.metadata;
936
+ const baseSeries = Array.isArray(metadata?.baseSeries) ? metadata.baseSeries : [];
937
+ return baseSeries.reduce((count, entry) => (
938
+ count + (isRecord(entry) && typeof entry.pointCount === "number" ? Math.max(0, entry.pointCount) : 0)
939
+ ), 0);
940
+ }
941
+ const graphKind = shotGraphKind(resolved);
942
+ if (graphKind) {
943
+ const metric = resolved.options.metric as GraphMetricKey;
944
+ const period = resolved.options.period as FundamentalPeriod;
945
+ const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
946
+ return payload.financials.reduce((count, [symbol, financials]) => (
947
+ count + limitGraphRowsBySymbol(
948
+ graphRowsForFinancials(financials, graphKind, metric, period, symbol),
949
+ periodCount,
950
+ ).length
951
+ ), 0);
952
+ }
953
+ if (["price-chart", "intraday-price-chart", "historical-prices"].includes(resolved.capability.id)) {
954
+ return payload.financials[0]?.[1].priceHistory.length ?? 0;
955
+ }
956
+ if (["price-comparison", "return-correlation", "security-relationship"].includes(resolved.capability.id)) {
957
+ return payload.financials.filter(([, financials]) => financials.priceHistory.length > 1).length;
958
+ }
959
+ if (resolved.capability.id === "financial-statements") {
960
+ const financials = payload.financials[0]?.[1];
961
+ const period = resolved.options.period;
962
+ return period === "quarterly"
963
+ ? financials?.quarterlyStatements.length ?? 0
964
+ : financials?.annualStatements.length ?? 0;
965
+ }
966
+ return payload.financials.filter(([, financials]) => !!financials.quote).length;
967
+ }
968
+
969
+ function shotExpectedText(
970
+ resolved: ResolvedPaneFunction,
971
+ symbols: string[],
972
+ payload: DesktopPaneShotPayload,
973
+ ): string[] {
974
+ const expected = [...symbols];
975
+ const graphKind = shotGraphKind(resolved);
976
+ if (graphKind) {
977
+ const metric = resolved.options.metric as GraphMetricKey;
978
+ const definition = metricDef(graphKind, metric);
979
+ const period = resolved.options.period as FundamentalPeriod;
980
+ const periodCount = resolved.options.periods == null ? null : Number(resolved.options.periods);
981
+ expected.push(definition.label);
982
+ if (resolved.pane.id === CHART_COMPOSER_PANE_ID) return expected.filter(Boolean);
983
+ for (const [symbol, financials] of payload.financials) {
984
+ const latestRow = limitGraphRowsBySymbol(
985
+ graphRowsForFinancials(financials, graphKind, metric, period, symbol),
986
+ periodCount,
987
+ ).sort((left, right) => left.date.localeCompare(right.date)).at(-1);
988
+ if (latestRow) {
989
+ expected.push(latestRow.date);
990
+ expected.push(definition.format(latestRow.value));
991
+ }
992
+ }
993
+ } else if (resolved.capability.id === "financial-statements") {
994
+ const statementLabels: Record<string, string> = {
995
+ income: "Income",
996
+ balance: "Balance",
997
+ cashflow: "Cash Flow",
998
+ };
999
+ expected.push(statementLabels[String(resolved.options.statement)] ?? "");
1000
+ expected.push(resolved.options.period === "annual" ? "Annual" : "Quarterly");
1001
+ const financials = payload.financials[0]?.[1];
1002
+ if (financials) {
1003
+ const table = buildFinancialTableModel(financials, {
1004
+ period: resolved.options.period as FundamentalPeriod,
1005
+ statement: String(resolved.options.statement),
1006
+ });
1007
+ const latestStatement = table?.statements[0];
1008
+ const firstMetric = table?.rows[0];
1009
+ if (latestStatement) expected.push(formatFinancialHeader(latestStatement.date).trim());
1010
+ if (firstMetric) expected.push(firstMetric.unitLabel);
1011
+ }
1012
+ }
1013
+ return expected.filter(Boolean);
1014
+ }
1015
+
1016
+ function shotExpectedSelections(
1017
+ resolved: ResolvedPaneFunction,
1018
+ ): PaneScreenshotExpectedSelection[] {
1019
+ if (resolved.pane.id === CHART_COMPOSER_PANE_ID) return [];
1020
+ if (shotGraphKind(resolved)) {
1021
+ return [{
1022
+ control: "metric",
1023
+ value: String(resolved.options.metric),
1024
+ }];
1025
+ }
1026
+ if (resolved.capability.id !== "financial-statements") return [];
1027
+ const labels: Record<string, string> = {
1028
+ income: "Income",
1029
+ cashflow: "Cash Flow",
1030
+ balance: "Balance Sheet",
1031
+ };
1032
+ return [
1033
+ {
1034
+ control: "statement",
1035
+ label: labels[String(resolved.options.statement)] ?? String(resolved.options.statement),
1036
+ },
1037
+ {
1038
+ control: "period",
1039
+ value: String(resolved.options.period),
1040
+ },
1041
+ ];
1042
+ }
1043
+
1044
+ export function missingActiveTabSelections(
1045
+ semanticUi: RemoteUiNodeSnapshot[],
1046
+ expected: PaneScreenshotExpectedSelection[],
1047
+ ): PaneScreenshotExpectedSelection[] {
1048
+ const activeTabs = semanticUi.flatMap((node) => {
1049
+ if (node.role !== "tabs" || !node.metadata) return [];
1050
+ const activeValue = typeof node.metadata.activeValue === "string"
1051
+ ? node.metadata.activeValue
1052
+ : null;
1053
+ const tabs = Array.isArray(node.metadata.tabs)
1054
+ ? node.metadata.tabs.filter((tab): tab is Record<string, unknown> => (
1055
+ !!tab && typeof tab === "object" && !Array.isArray(tab)
1056
+ ))
1057
+ : [];
1058
+ const activeTab = tabs.find((tab) => String(tab.value) === activeValue);
1059
+ return [{
1060
+ value: activeValue,
1061
+ label: typeof activeTab?.label === "string" ? activeTab.label : null,
1062
+ }];
1063
+ });
1064
+
1065
+ return expected.filter((selection) => !activeTabs.some((active) => (
1066
+ (selection.value == null || active.value === selection.value)
1067
+ && (selection.label == null || active.label?.toLowerCase() === selection.label.toLowerCase())
1068
+ )));
125
1069
  }