gloomberb 0.11.0 → 0.11.2

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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -7,7 +7,7 @@ import type { BrokerConnectionStatus } from "../../../../types/broker";
7
7
  import type { TickerFinancials } from "../../../../types/financials";
8
8
  import type { Portfolio, TickerRecord } from "../../../../types/ticker";
9
9
  import type { BrokerAccount, BrokerCashBalance } from "../../../../types/trading";
10
- import { displayWidth, formatCompact, formatPercentRaw } from "../../../../utils/format";
10
+ import { convertCurrency, displayWidth, formatCompact, formatPercentRaw } from "../../../../utils/format";
11
11
  import { getBrokerInstance } from "../../../../utils/broker-instances";
12
12
  import { resolvePortfolioAccountMetrics, resolvePortfolioMarketValue } from "../account-metrics";
13
13
  import { calculatePortfolioSummaryTotals, type PortfolioSummaryTotals } from "./totals";
@@ -188,21 +188,23 @@ export function buildPortfolioSummarySegments({
188
188
  accountStatusText,
189
189
  widthBudget,
190
190
  refreshText,
191
+ convertAccountValue = (value) => value,
191
192
  }: {
192
193
  totals: PortfolioSummaryTotals;
193
194
  accountState: PortfolioSummaryAccountState | null;
194
195
  accountStatusText?: string;
195
196
  widthBudget: number;
196
197
  refreshText?: string;
198
+ convertAccountValue?: (value: number) => number;
197
199
  }): PortfolioSummarySegment[] {
198
200
  const candidates: PortfolioSummarySegment[] = [];
199
- const accountMetrics = resolvePortfolioAccountMetrics(totals, accountState?.account);
200
- const totalMarketValue = resolvePortfolioMarketValue(totals, accountState?.account);
201
+ const accountMetrics = resolvePortfolioAccountMetrics(totals, accountState?.account, convertAccountValue);
202
+ const totalMarketValue = resolvePortfolioMarketValue(totals, accountState?.account, convertAccountValue);
201
203
 
202
204
  if (accountState?.account.netLiquidation != null) {
203
205
  candidates.push(createSummarySegment("netliq", [
204
206
  { text: "Net Liq", tone: "label" },
205
- { text: formatCompact(accountState.account.netLiquidation), tone: "value", bold: true },
207
+ { text: formatCompact(convertAccountValue(accountState.account.netLiquidation)), tone: "value", bold: true },
206
208
  ]));
207
209
  }
208
210
 
@@ -214,7 +216,7 @@ export function buildPortfolioSummarySegments({
214
216
  if (accountState?.account.totalCashValue != null) {
215
217
  candidates.push(createSummarySegment("cash", [
216
218
  { text: "Cash", tone: "label" },
217
- { text: formatCompact(accountState.account.totalCashValue), tone: "value", bold: true },
219
+ { text: formatCompact(convertAccountValue(accountState.account.totalCashValue)), tone: "value", bold: true },
218
220
  ]));
219
221
  }
220
222
 
@@ -241,25 +243,25 @@ export function buildPortfolioSummarySegments({
241
243
  account.settledCash != null
242
244
  ? createSummarySegment("settled", [
243
245
  { text: "Settled", tone: "label" },
244
- { text: formatCompact(account.settledCash), tone: "value", bold: true },
246
+ { text: formatCompact(convertAccountValue(account.settledCash)), tone: "value", bold: true },
245
247
  ])
246
248
  : null,
247
249
  account.availableFunds != null
248
250
  ? createSummarySegment("avail", [
249
251
  { text: "Avail", tone: "label" },
250
- { text: formatCompact(account.availableFunds), tone: "value", bold: true },
252
+ { text: formatCompact(convertAccountValue(account.availableFunds)), tone: "value", bold: true },
251
253
  ])
252
254
  : null,
253
255
  account.excessLiquidity != null
254
256
  ? createSummarySegment("excess", [
255
257
  { text: "Excess", tone: "label" },
256
- { text: formatCompact(account.excessLiquidity), tone: "value", bold: true },
258
+ { text: formatCompact(convertAccountValue(account.excessLiquidity)), tone: "value", bold: true },
257
259
  ])
258
260
  : null,
259
261
  account.buyingPower != null
260
262
  ? createSummarySegment("bp", [
261
263
  { text: "BP", tone: "label" },
262
- { text: formatCompact(account.buyingPower), tone: "value", bold: true },
264
+ { text: formatCompact(convertAccountValue(account.buyingPower)), tone: "value", bold: true },
263
265
  ])
264
266
  : null,
265
267
  createSummarySegment("source", [
@@ -348,12 +350,19 @@ export function buildPortfolioFooterSegments({
348
350
 
349
351
  // An account error must still reach the footer, otherwise a failed load looks like an empty portfolio.
350
352
  if (!totals.hasPositions && !accountState && !accountStatusText) return [];
353
+ const convertAccountValue = (value: number) => convertCurrency(
354
+ value,
355
+ accountState?.account.currency || baseCurrency,
356
+ baseCurrency,
357
+ exchangeRates,
358
+ );
351
359
  return buildPortfolioSummarySegments({
352
360
  totals,
353
361
  accountState,
354
362
  accountStatusText,
355
363
  widthBudget: Math.max(16, width - 14),
356
364
  refreshText,
365
+ convertAccountValue,
357
366
  }).map((segment) => ({
358
367
  id: segment.id,
359
368
  parts: segment.parts,
@@ -384,40 +393,45 @@ export function renderSummarySegments(segments: PortfolioSummarySegment[], width
384
393
  );
385
394
  }
386
395
 
387
- export function buildDrawerMetricSegments(account: BrokerAccount, widthBudget: number): PortfolioSummarySegment[] {
396
+ export function buildDrawerMetricSegments(
397
+ account: BrokerAccount,
398
+ widthBudget: number,
399
+ convertAccountValue: (value: number) => number = (value) => value,
400
+ ): PortfolioSummarySegment[] {
401
+ const money = (value: number) => convertAccountValue(value);
388
402
  const candidates = [
389
403
  account.dailyPnl != null
390
- ? createSummarySegment("day", [{ text: "Day", tone: "label" }, { text: formatSignedCompact(account.dailyPnl), tone: "value", color: priceColor(account.dailyPnl), bold: true }])
404
+ ? createSummarySegment("day", [{ text: "Day", tone: "label" }, { text: formatSignedCompact(money(account.dailyPnl)), tone: "value", color: priceColor(money(account.dailyPnl)), bold: true }])
391
405
  : null,
392
406
  account.unrealizedPnl != null
393
- ? createSummarySegment("unreal", [{ text: "Unreal", tone: "label" }, { text: formatSignedCompact(account.unrealizedPnl), tone: "value", color: priceColor(account.unrealizedPnl), bold: true }])
407
+ ? createSummarySegment("unreal", [{ text: "Unreal", tone: "label" }, { text: formatSignedCompact(money(account.unrealizedPnl)), tone: "value", color: priceColor(money(account.unrealizedPnl)), bold: true }])
394
408
  : null,
395
409
  account.realizedPnl != null
396
- ? createSummarySegment("realized", [{ text: "Realized", tone: "label" }, { text: formatSignedCompact(account.realizedPnl), tone: "value", color: priceColor(account.realizedPnl), bold: true }])
410
+ ? createSummarySegment("realized", [{ text: "Realized", tone: "label" }, { text: formatSignedCompact(money(account.realizedPnl)), tone: "value", color: priceColor(money(account.realizedPnl)), bold: true }])
397
411
  : null,
398
412
  account.totalCashValue != null
399
- ? createSummarySegment("cash", [{ text: "Cash", tone: "label" }, { text: formatCompact(account.totalCashValue), tone: "value", bold: true }])
413
+ ? createSummarySegment("cash", [{ text: "Cash", tone: "label" }, { text: formatCompact(money(account.totalCashValue)), tone: "value", bold: true }])
400
414
  : null,
401
415
  account.settledCash != null
402
- ? createSummarySegment("settled", [{ text: "Settled", tone: "label" }, { text: formatCompact(account.settledCash), tone: "value", bold: true }])
416
+ ? createSummarySegment("settled", [{ text: "Settled", tone: "label" }, { text: formatCompact(money(account.settledCash)), tone: "value", bold: true }])
403
417
  : null,
404
418
  account.netLiquidation != null
405
- ? createSummarySegment("netliq", [{ text: "Net Liq", tone: "label" }, { text: formatCompact(account.netLiquidation), tone: "value", bold: true }])
419
+ ? createSummarySegment("netliq", [{ text: "Net Liq", tone: "label" }, { text: formatCompact(money(account.netLiquidation)), tone: "value", bold: true }])
406
420
  : null,
407
421
  account.availableFunds != null
408
- ? createSummarySegment("avail", [{ text: "Avail", tone: "label" }, { text: formatCompact(account.availableFunds), tone: "value", bold: true }])
422
+ ? createSummarySegment("avail", [{ text: "Avail", tone: "label" }, { text: formatCompact(money(account.availableFunds)), tone: "value", bold: true }])
409
423
  : null,
410
424
  account.excessLiquidity != null
411
- ? createSummarySegment("excess", [{ text: "Excess", tone: "label" }, { text: formatCompact(account.excessLiquidity), tone: "value", bold: true }])
425
+ ? createSummarySegment("excess", [{ text: "Excess", tone: "label" }, { text: formatCompact(money(account.excessLiquidity)), tone: "value", bold: true }])
412
426
  : null,
413
427
  account.buyingPower != null
414
- ? createSummarySegment("bp", [{ text: "BP", tone: "label" }, { text: formatCompact(account.buyingPower), tone: "value", bold: true }])
428
+ ? createSummarySegment("bp", [{ text: "BP", tone: "label" }, { text: formatCompact(money(account.buyingPower)), tone: "value", bold: true }])
415
429
  : null,
416
430
  account.initMarginReq != null
417
- ? createSummarySegment("init", [{ text: "Init", tone: "label" }, { text: formatCompact(account.initMarginReq), tone: "value", bold: true }])
431
+ ? createSummarySegment("init", [{ text: "Init", tone: "label" }, { text: formatCompact(money(account.initMarginReq)), tone: "value", bold: true }])
418
432
  : null,
419
433
  account.maintMarginReq != null
420
- ? createSummarySegment("maint", [{ text: "Maint", tone: "label" }, { text: formatCompact(account.maintMarginReq), tone: "value", bold: true }])
434
+ ? createSummarySegment("maint", [{ text: "Maint", tone: "label" }, { text: formatCompact(money(account.maintMarginReq)), tone: "value", bold: true }])
421
435
  : null,
422
436
  ].filter((segment): segment is PortfolioSummarySegment => segment != null);
423
437
 
@@ -2,7 +2,12 @@ import type { TickerFinancials } from "../../../../types/financials";
2
2
  import type { TickerRecord } from "../../../../types/ticker";
3
3
  import { convertCurrency } from "../../../../utils/format";
4
4
  import { getActiveQuoteDisplay } from "../../../../market-data/market/status";
5
- import { getPortfolioPositionMetrics, resolveBrokerFallbackMarketValue } from "../position-metrics";
5
+ import {
6
+ getPortfolioPositionMetrics,
7
+ resolveBrokerFallbackMarketValue,
8
+ resolveBrokerFallbackPnl,
9
+ signedQuoteUnrealizedPnl,
10
+ } from "../position-metrics";
6
11
 
7
12
  export interface PortfolioSummaryTotals {
8
13
  totalMktValue: number;
@@ -27,6 +32,8 @@ export function calculatePortfolioSummaryTotals(
27
32
  let totalMktValue = 0;
28
33
  let totalPrevValue = 0;
29
34
  let totalCostBasis = 0;
35
+ let signedDailyPnl = 0;
36
+ let signedUnrealizedPnl = 0;
30
37
  let hasPositions = false;
31
38
  let watchlistChangeSum = 0;
32
39
  let watchlistCount = 0;
@@ -54,21 +61,28 @@ export function calculatePortfolioSummaryTotals(
54
61
  if (quote && activeQuote && totalPriceUnits !== 0) {
55
62
  hasPositions = true;
56
63
  const marketValue = Math.abs(totalPriceUnits) * activeQuote.price;
57
- totalMktValue += toBaseQuote(marketValue);
58
64
  const previousClose = quote.previousClose || (activeQuote.price - activeQuote.change);
59
- totalPrevValue += toBaseQuote(Math.abs(totalPriceUnits) * previousClose);
60
- totalCostBasis += toBasePosition(totalCost);
65
+ const previousValue = Math.abs(totalPriceUnits) * previousClose;
66
+ const convertedMarket = toBaseQuote(marketValue);
67
+ const convertedCost = toBasePosition(totalCost);
68
+ totalMktValue += convertedMarket;
69
+ totalPrevValue += toBaseQuote(previousValue);
70
+ totalCostBasis += convertedCost;
71
+ signedDailyPnl += toBaseQuote(totalPriceUnits * (activeQuote.price - previousClose));
72
+ signedUnrealizedPnl += signedQuoteUnrealizedPnl(convertedMarket, convertedCost, totalPriceUnits);
61
73
  } else if (brokerFallbackMktValue != null) {
62
74
  hasPositions = true;
63
75
  totalMktValue += toBasePosition(brokerFallbackMktValue);
64
76
  totalCostBasis += toBasePosition(totalCost);
65
77
  totalPrevValue += toBasePosition(brokerFallbackMktValue);
78
+ const brokerPnl = resolveBrokerFallbackPnl(positionMetrics, brokerFallbackMktValue);
79
+ if (brokerPnl != null) signedUnrealizedPnl += toBasePosition(brokerPnl);
66
80
  }
67
81
  }
68
82
 
69
- const dailyPnl = totalMktValue - totalPrevValue;
83
+ const dailyPnl = signedDailyPnl;
70
84
  const dailyPnlPct = totalPrevValue !== 0 ? (dailyPnl / totalPrevValue) * 100 : 0;
71
- const unrealizedPnl = totalMktValue - totalCostBasis;
85
+ const unrealizedPnl = signedUnrealizedPnl;
72
86
  const unrealizedPnlPct = totalCostBasis !== 0 ? (unrealizedPnl / totalCostBasis) * 100 : 0;
73
87
  const avgWatchlistChange = watchlistCount > 0 ? watchlistChangeSum / watchlistCount : 0;
74
88
 
@@ -23,6 +23,7 @@ import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from
23
23
  import { instrumentFromTicker } from "../../../market-data/request-types";
24
24
  import { usePaneTicker } from "../../../state/app/context";
25
25
  import { isUsEquityTicker } from "../../../utils/sec";
26
+ import { computeTTM } from "../ticker-detail/financials/aggregation";
26
27
  import { useAssetData } from "../../runtime";
27
28
  import { handleRefreshKey, loadingErrorFooterInfo } from "../shared/table-pane";
28
29
  import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
@@ -241,16 +242,8 @@ function estimateValue(pair: EstimatePair): string {
241
242
 
242
243
  function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow | null {
243
244
  const latestFour = quarterlyStatements.slice(-4);
244
- if (latestFour.length < 4) return null;
245
- const fiscalRevenue = latestFour.reduce<number | undefined>((total, statement) => {
246
- if (statement.totalRevenue == null) return total;
247
- return (total ?? 0) + statement.totalRevenue;
248
- }, undefined);
249
- const fiscalEps = latestFour.reduce<number | undefined>((total, statement) => {
250
- if (statement.eps == null) return total;
251
- return (total ?? 0) + statement.eps;
252
- }, undefined);
253
- if (fiscalRevenue == null && fiscalEps == null) return null;
245
+ const ttm = computeTTM([...quarterlyStatements]);
246
+ if (!ttm || (ttm.totalRevenue == null && ttm.eps == null)) return null;
254
247
  const latest = latestFour.at(-1);
255
248
  return {
256
249
  id: `ttm:${latest?.date ?? ""}`,
@@ -258,8 +251,8 @@ function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow |
258
251
  status: "TTM",
259
252
  period: "4 qtrs",
260
253
  detail: "sum",
261
- annualEps: fiscalEps,
262
- annualRevenue: fiscalRevenue,
254
+ annualEps: ttm.eps,
255
+ annualRevenue: ttm.totalRevenue,
263
256
  value: "-",
264
257
  tone: "muted",
265
258
  };
@@ -0,0 +1,329 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { apiClient, setCloudApiFetchTransport } from "../../../api-client";
4
+ import type { CloudEquityDiagnosticResponse } from "../../../api-client";
5
+ import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane/footer";
6
+ import { testRender } from "../../../renderers/opentui/test-utils";
7
+ import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app/context";
8
+ import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
9
+ import { cloneLayout, createDefaultConfig } from "../../../types/config";
10
+ import type { TickerRecord } from "../../../types/ticker";
11
+ import { Box } from "../../../ui";
12
+ import { PluginRenderProvider } from "../../runtime";
13
+ import { EquityDiagnosticView } from "./equity-diagnostic-pane";
14
+
15
+ const TEST_PANE_ID = "equity-diagnostic:AAPL";
16
+ const WIDTH = 96;
17
+ /** Tall enough that a full report fits without scrolling the assertions away. */
18
+ const HEIGHT = 44;
19
+
20
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
21
+
22
+ function makeTicker(symbol: string): TickerRecord {
23
+ return {
24
+ metadata: {
25
+ ticker: symbol,
26
+ exchange: "NASDAQ",
27
+ currency: "USD",
28
+ name: symbol,
29
+ portfolios: [],
30
+ watchlists: [],
31
+ positions: [],
32
+ custom: {},
33
+ tags: [],
34
+ },
35
+ };
36
+ }
37
+
38
+ function signIn(plan: "free" | "pro"): void {
39
+ apiClient.setSessionToken("equity-diagnostic-test-token");
40
+ apiClient.restoreCachedUser({
41
+ id: "user-1",
42
+ name: "Tester",
43
+ email: "tester@example.com",
44
+ username: "tester",
45
+ emailVerified: true,
46
+ plan,
47
+ } as never);
48
+ }
49
+
50
+ /** Answers the diagnostic endpoint locally and records the request bodies. */
51
+ function mockDiagnosticTransport(
52
+ respond: (body: { symbol: string; exchange?: string; mode?: string }) => Response,
53
+ ): Array<{ symbol: string; exchange?: string; mode?: string }> {
54
+ const requests: Array<{ symbol: string; exchange?: string; mode?: string }> = [];
55
+ setCloudApiFetchTransport(async (url, init) => {
56
+ if (!url.endsWith("/research/equity-diagnostic")) {
57
+ throw new Error(`unexpected cloud request: ${url}`);
58
+ }
59
+ const body = JSON.parse(String(init?.body ?? "{}"));
60
+ requests.push(body);
61
+ return respond(body);
62
+ });
63
+ return requests;
64
+ }
65
+
66
+ function jsonResponse(body: unknown, status = 200): Response {
67
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
68
+ }
69
+
70
+ function makeReport(overrides: Partial<CloudEquityDiagnosticResponse> = {}): CloudEquityDiagnosticResponse {
71
+ return {
72
+ schemaVersion: 1,
73
+ access: "full",
74
+ symbol: "AAPL",
75
+ exchange: "NASDAQ",
76
+ companyName: "Apple Inc.",
77
+ status: "partial",
78
+ verdict: "risk_skewed",
79
+ summary: "Margins slipped while inventory built up.",
80
+ confidence: 0.72,
81
+ findings: [
82
+ {
83
+ id: "f-low",
84
+ kind: "red_flag",
85
+ severity: 1,
86
+ confidence: 0.4,
87
+ title: "Receivables grew faster than sales",
88
+ observation: "Receivables rose 18 percent against 4 percent revenue growth.",
89
+ interpretation: "Collection may be slowing.",
90
+ evidenceIds: [],
91
+ },
92
+ {
93
+ id: "f-high",
94
+ kind: "red_flag",
95
+ severity: 3,
96
+ confidence: 0.81,
97
+ title: "Gross margin fell for three quarters",
98
+ observation: "Gross margin fell to 38.2 percent from 44.6 percent.",
99
+ interpretation: "Pricing pressure is outpacing mix improvement.",
100
+ evidenceIds: ["ev-10k", "ev-missing"],
101
+ },
102
+ {
103
+ id: "f-green",
104
+ kind: "green_flag",
105
+ severity: 2,
106
+ confidence: 0.6,
107
+ title: "Buyback pace accelerated",
108
+ observation: "Shares outstanding fell 3 percent.",
109
+ interpretation: "Capital return is intact.",
110
+ evidenceIds: ["ev-news"],
111
+ },
112
+ ],
113
+ watchItems: ["Next quarter gross margin guidance"],
114
+ coverage: [
115
+ { dataset: "financials", status: "available", asOf: "2026-02-01", provider: "Twelve Data" },
116
+ { dataset: "insider", status: "no_data" },
117
+ ],
118
+ evidence: [
119
+ { id: "ev-10k", dataset: "filings", label: "10-K", asOf: "2026-02-01", url: "https://sec.gov/aapl-10k" },
120
+ { id: "ev-news", dataset: "news", label: "Reuters" },
121
+ ],
122
+ generatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
123
+ expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
124
+ refreshAllowedAt: new Date(Date.now() + 600_000).toISOString(),
125
+ cached: true,
126
+ stale: true,
127
+ promptVersion: 1,
128
+ model: "gpt-5.6-luna",
129
+ ...overrides,
130
+ };
131
+ }
132
+
133
+ function DiagnosticHarness() {
134
+ const config = createDefaultConfig("/tmp/gloomberb-equity-diagnostic-test");
135
+ config.layout = {
136
+ dockRoot: { kind: "pane", instanceId: TEST_PANE_ID },
137
+ instances: [{
138
+ instanceId: TEST_PANE_ID,
139
+ paneId: "equity-diagnostic",
140
+ binding: { kind: "fixed", symbol: "AAPL" },
141
+ }],
142
+ floating: [],
143
+ detached: [],
144
+ };
145
+ config.layouts = [{ name: "Default", layout: cloneLayout(config.layout) }];
146
+
147
+ const state = createInitialState(config);
148
+ state.focusedPaneId = TEST_PANE_ID;
149
+ state.tickers = new Map([["AAPL", makeTicker("AAPL")]]);
150
+
151
+ return (
152
+ <AppContext value={{ state, dispatch: () => {} }}>
153
+ <PaneInstanceProvider paneId={TEST_PANE_ID}>
154
+ <PluginRenderProvider pluginId="ticker-research" runtime={createTestPluginRuntime()}>
155
+ <PaneFooterProvider>
156
+ {(footer) => (
157
+ <Box flexDirection="column" width={WIDTH} height={HEIGHT}>
158
+ <EquityDiagnosticView width={WIDTH} height={HEIGHT - 1} focused />
159
+ <PaneFooterBar footer={footer} focused width={WIDTH} />
160
+ </Box>
161
+ )}
162
+ </PaneFooterProvider>
163
+ </PluginRenderProvider>
164
+ </PaneInstanceProvider>
165
+ </AppContext>
166
+ );
167
+ }
168
+
169
+ async function renderHarness(): Promise<void> {
170
+ await act(async () => {
171
+ testSetup = await testRender(<DiagnosticHarness />, { width: WIDTH, height: HEIGHT });
172
+ });
173
+ await settle();
174
+ }
175
+
176
+ async function settle(): Promise<void> {
177
+ for (let attempt = 0; attempt < 6; attempt += 1) {
178
+ await act(async () => {
179
+ await Bun.sleep(1);
180
+ await testSetup!.renderOnce();
181
+ });
182
+ }
183
+ }
184
+
185
+ async function pressKey(name: string): Promise<void> {
186
+ await act(async () => {
187
+ testSetup!.renderer.keyInput.emit("keypress", {
188
+ name,
189
+ sequence: name,
190
+ ctrl: false,
191
+ meta: false,
192
+ option: false,
193
+ shift: false,
194
+ eventType: "press",
195
+ repeated: false,
196
+ stopPropagation: () => {},
197
+ preventDefault: () => {},
198
+ } as never);
199
+ await testSetup!.renderOnce();
200
+ });
201
+ await settle();
202
+ }
203
+
204
+ afterEach(async () => {
205
+ if (testSetup) {
206
+ await act(async () => testSetup!.renderer.destroy());
207
+ testSetup = undefined;
208
+ }
209
+ setCloudApiFetchTransport(null);
210
+ apiClient.setSessionToken(null);
211
+ apiClient.restoreCachedUser(null);
212
+ });
213
+
214
+ test("shows a cited preview to free accounts and gates the rest", async () => {
215
+ signIn("free");
216
+ const fullReport = makeReport();
217
+ const requests = mockDiagnosticTransport(() => jsonResponse(makeReport({
218
+ access: "preview",
219
+ verdict: "unclear",
220
+ summary: "",
221
+ confidence: 0.81,
222
+ findings: [fullReport.findings[1]!],
223
+ watchItems: [],
224
+ evidence: [fullReport.evidence[0]!],
225
+ })));
226
+
227
+ await renderHarness();
228
+
229
+ const frame = testSetup!.captureCharFrame();
230
+ expect(requests).toEqual([{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" }]);
231
+ expect(frame).toContain("Free preview");
232
+ expect(frame).toContain("Gross margin fell for three quarters");
233
+ expect(frame).not.toContain("Receivables grew faster than sales");
234
+ expect(frame).not.toContain("Risk skewed");
235
+ expect(frame).toContain("UNLOCK THE FULL DIAGNOSTIC");
236
+ expect(frame).toContain("Upgrade to Pro");
237
+ expect(frame).toContain("Gloom Cloud");
238
+ expect(frame).not.toContain("Twelve Data");
239
+ expect(frame).not.toContain("efresh");
240
+ });
241
+
242
+ test("renders a stale partial report with severity order, split observation, and citations", async () => {
243
+ signIn("pro");
244
+ mockDiagnosticTransport(() => jsonResponse(makeReport()));
245
+
246
+ await renderHarness();
247
+
248
+ const frame = testSetup!.captureCharFrame();
249
+ expect(frame).toContain("Risk skewed");
250
+ expect(frame).toContain("Apple Inc.");
251
+ expect(frame).toContain("4m ago");
252
+ expect(frame).toContain("confidence 72%");
253
+ expect(frame).toContain("Margins slipped");
254
+
255
+ // Severity sorted inside a kind, and split into observation versus reading.
256
+ expect(frame.indexOf("Gross margin fell for three quarters"))
257
+ .toBeLessThan(frame.indexOf("Receivables grew faster than sales"));
258
+ expect(frame).toContain("Observed");
259
+ expect(frame).toContain("Reading");
260
+ expect(frame).toContain("Pricing pressure is outpacing mix improvement.");
261
+
262
+ // Citations resolve against the server catalog; unknown ids are dropped.
263
+ expect(frame).toContain("10-K 2026-02-01");
264
+ expect(frame).toContain("Reuters");
265
+ expect(frame).not.toContain("ev-missing");
266
+
267
+ expect(frame).toContain("WATCH ITEMS");
268
+ expect(frame).toContain("COVERAGE");
269
+ expect(frame).toContain("no data");
270
+
271
+ // Footer carries changing state only, and the model stays an implementation detail.
272
+ expect(frame).toContain("partial");
273
+ expect(frame).toContain("stale");
274
+ expect(frame).toContain("efresh");
275
+ expect(frame).not.toContain("luna");
276
+ });
277
+
278
+ test("adds data sources line by line while the diagnostic generates", async () => {
279
+ signIn("pro");
280
+ mockDiagnosticTransport(() => jsonResponse({ status: "generating", retryAfterMs: 5_000 }, 202));
281
+
282
+ await renderHarness();
283
+
284
+ const frame = testSetup!.captureCharFrame();
285
+ expect(frame).toContain("Gloom Cloud market data");
286
+ expect(frame).toContain("SEC EDGAR filings");
287
+ expect(frame).not.toContain("FINRA short interest");
288
+ });
289
+
290
+ test("polls an uncached diagnostic until the background report is ready", async () => {
291
+ signIn("pro");
292
+ let attempts = 0;
293
+ const requests = mockDiagnosticTransport(() => {
294
+ attempts += 1;
295
+ return attempts === 1
296
+ ? jsonResponse({ status: "generating", retryAfterMs: 10 }, 202)
297
+ : jsonResponse(makeReport());
298
+ });
299
+
300
+ await renderHarness();
301
+ await Bun.sleep(20);
302
+ await settle();
303
+
304
+ expect(requests).toEqual([
305
+ { symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" },
306
+ { symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" },
307
+ ]);
308
+ expect(testSetup!.captureCharFrame()).toContain("Gross margin fell for three quarters");
309
+ });
310
+
311
+ test("asks for a refresh on r and keeps the last report when the retry is rate limited", async () => {
312
+ signIn("pro");
313
+ const requests = mockDiagnosticTransport((body) => (
314
+ body.mode === "refresh"
315
+ ? jsonResponse({ message: "Too many requests" }, 429)
316
+ : jsonResponse(makeReport())
317
+ ));
318
+
319
+ await renderHarness();
320
+ expect(requests).toEqual([{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" }]);
321
+
322
+ await pressKey("r");
323
+
324
+ expect(requests.at(-1)?.mode).toBe("refresh");
325
+ const frame = testSetup!.captureCharFrame();
326
+ expect(frame).toContain("Rate limited");
327
+ expect(frame).toContain("Gross margin fell for three quarters");
328
+ expect(frame).toContain("Retry");
329
+ });