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
@@ -0,0 +1,582 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import { Box, ScrollBox, Text, TextAttributes, useUiCapabilities } from "../../../ui";
3
+ import { Button, EmptyState, Spinner, usePaneFooter } from "../../../components";
4
+ import { ExternalLinkText } from "../../../components/ui";
5
+ import { CloudAuthNotice } from "../cloud/auth-actions";
6
+ import { apiClient } from "../../../api-client";
7
+ import type {
8
+ CloudEquityDiagnosticCoverage,
9
+ CloudEquityDiagnosticEvidence,
10
+ CloudEquityDiagnosticFinding,
11
+ CloudEquityDiagnosticFindingKind,
12
+ CloudEquityDiagnosticMode,
13
+ CloudEquityDiagnosticResponse,
14
+ } from "../../../api-client";
15
+ import { ApiRequestError } from "../../../api-client/errors";
16
+ import { colors } from "../../../theme/colors";
17
+ import { t, tf } from "../../../i18n";
18
+ import { useShortcut } from "../../../react/input";
19
+ import { formatTimeAgo, truncateToDisplayWidth } from "../../../utils/format";
20
+ import { isPlainKey } from "../../../utils/keyboard";
21
+ import { usePlanAccess } from "../shared/plan-access";
22
+ import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade";
23
+ import { useBoundTicker } from "../shared/ticker-request";
24
+
25
+ const FOOTER_ID = "equity-diagnostic";
26
+ const REFRESH_SCOPE = "equity-diagnostic:refresh";
27
+ /** Width of the "Observed"/"Reading" label column before the layout stacks. */
28
+ const LABEL_WIDTH = 10;
29
+ const STACK_BELOW_WIDTH = 44;
30
+ const COVERAGE_LABEL_WIDTH = 16;
31
+ const LOADING_STEPS = [
32
+ "Gloom Cloud market data",
33
+ "SEC EDGAR filings",
34
+ "FINRA short interest",
35
+ "Gloom News",
36
+ "Reviewing evidence",
37
+ ] as const;
38
+
39
+ interface DiagnosticFailure {
40
+ status?: number;
41
+ message: string;
42
+ }
43
+
44
+ function toFailure(error: unknown): DiagnosticFailure {
45
+ return {
46
+ status: error instanceof ApiRequestError ? error.status : undefined,
47
+ message: error instanceof Error ? error.message : String(error),
48
+ };
49
+ }
50
+
51
+ function failureText(failure: DiagnosticFailure): string {
52
+ if (failure.status === 429) return t("Rate limited. Try again in a moment.");
53
+ if (failure.status != null && failure.status >= 500) return t("Diagnostic service is unavailable.");
54
+ return failure.message;
55
+ }
56
+
57
+ /**
58
+ * One report request per visited surface. A failed refresh keeps the report the
59
+ * user is already reading, so a rate limit or an outage never blanks the pane.
60
+ */
61
+ function useEquityDiagnostic(symbol: string | null, exchange: string, enabled: boolean) {
62
+ const [state, setState] = useState<{
63
+ report: CloudEquityDiagnosticResponse | null;
64
+ loading: boolean;
65
+ loadingStep: number;
66
+ failure: DiagnosticFailure | null;
67
+ }>({ report: null, loading: false, loadingStep: 0, failure: null });
68
+ const generationRef = useRef(0);
69
+ const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
70
+
71
+ const clearPoll = useCallback(() => {
72
+ if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
73
+ pollTimerRef.current = null;
74
+ }, []);
75
+
76
+ const load = useCallback((mode: CloudEquityDiagnosticMode) => {
77
+ if (!symbol || !enabled) return;
78
+ clearPoll();
79
+ generationRef.current += 1;
80
+ const generation = generationRef.current;
81
+ setState((current) => ({ ...current, loading: true, loadingStep: 1, failure: null }));
82
+
83
+ const request = (nextMode: CloudEquityDiagnosticMode) => {
84
+ apiClient.getCloudEquityDiagnostic(symbol, exchange || undefined, nextMode)
85
+ .then((result) => {
86
+ if (generationRef.current !== generation) return;
87
+ if (result.status === "generating") {
88
+ setState((current) => ({
89
+ ...current,
90
+ loadingStep: Math.min(LOADING_STEPS.length, current.loadingStep + 1),
91
+ }));
92
+ const retryAfterMs = Math.max(10, Math.min(5_000, result.retryAfterMs));
93
+ pollTimerRef.current = setTimeout(() => request("cache-first"), retryAfterMs);
94
+ return;
95
+ }
96
+ clearPoll();
97
+ setState({ report: result, loading: false, loadingStep: 0, failure: null });
98
+ })
99
+ .catch((error: unknown) => {
100
+ if (generationRef.current !== generation) return;
101
+ clearPoll();
102
+ setState((current) => ({
103
+ report: current.report,
104
+ loading: false,
105
+ loadingStep: 0,
106
+ failure: toFailure(error),
107
+ }));
108
+ });
109
+ };
110
+
111
+ request(mode);
112
+ }, [clearPoll, enabled, exchange, symbol]);
113
+
114
+ useEffect(() => {
115
+ // A report belongs to one company, so drop it rather than show it under the next.
116
+ generationRef.current += 1;
117
+ clearPoll();
118
+ setState({ report: null, loading: false, loadingStep: 0, failure: null });
119
+ load("cache-first");
120
+ return clearPoll;
121
+ }, [clearPoll, load]);
122
+
123
+ return { ...state, load };
124
+ }
125
+
126
+ function verdictLabel(verdict: CloudEquityDiagnosticResponse["verdict"]): string {
127
+ switch (verdict) {
128
+ case "risk_skewed": return t("Risk skewed");
129
+ case "opportunity_skewed": return t("Opportunity skewed");
130
+ case "balanced": return t("Balanced");
131
+ default: return t("Unclear");
132
+ }
133
+ }
134
+
135
+ function verdictColor(verdict: CloudEquityDiagnosticResponse["verdict"]): string {
136
+ switch (verdict) {
137
+ case "risk_skewed": return colors.negative;
138
+ case "opportunity_skewed": return colors.positive;
139
+ case "balanced": return colors.text;
140
+ default: return colors.textDim;
141
+ }
142
+ }
143
+
144
+ function findingColor(kind: CloudEquityDiagnosticFindingKind): string {
145
+ switch (kind) {
146
+ case "red_flag": return colors.negative;
147
+ case "green_flag": return colors.positive;
148
+ default: return colors.warning;
149
+ }
150
+ }
151
+
152
+ function severityLabel(severity: CloudEquityDiagnosticFinding["severity"]): string {
153
+ if (severity >= 3) return t("HIGH");
154
+ if (severity === 2) return t("MEDIUM");
155
+ return t("LOW");
156
+ }
157
+
158
+ function coverageColor(status: CloudEquityDiagnosticCoverage["status"]): string {
159
+ if (status === "available") return colors.textDim;
160
+ return status === "failed" ? colors.warning : colors.textMuted;
161
+ }
162
+
163
+ function coverageLabel(status: CloudEquityDiagnosticCoverage["status"]): string {
164
+ switch (status) {
165
+ case "available": return t("available");
166
+ case "no_data": return t("no data");
167
+ case "unsupported": return t("unsupported");
168
+ default: return t("failed");
169
+ }
170
+ }
171
+
172
+ function percent(value: number): string {
173
+ const scaled = value <= 1 ? value * 100 : value;
174
+ return `${Math.round(scaled)}%`;
175
+ }
176
+
177
+ function citationLabel(evidence: CloudEquityDiagnosticEvidence): string {
178
+ const date = evidence.asOf?.slice(0, 10);
179
+ return date && !evidence.label.includes(date) ? `${evidence.label} ${date}` : evidence.label;
180
+ }
181
+
182
+ function datasetLabel(dataset: string): string {
183
+ return dataset.replaceAll("_", " ");
184
+ }
185
+
186
+ function providerLabel(provider?: string): string | undefined {
187
+ if (!provider) return undefined;
188
+ if (provider === "Gloomberb News") return "Gloom News";
189
+ if (provider.includes("Twelve Data") || provider === "Market data providers") return "Gloom Cloud";
190
+ return provider;
191
+ }
192
+
193
+ function sortFindings(findings: readonly CloudEquityDiagnosticFinding[]): CloudEquityDiagnosticFinding[] {
194
+ return [...findings].sort((left, right) => (
195
+ right.severity - left.severity || right.confidence - left.confidence
196
+ ));
197
+ }
198
+
199
+ function Paragraph({ text: value, width, color, bold }: {
200
+ text: string;
201
+ width: number;
202
+ color: string;
203
+ bold?: boolean;
204
+ }) {
205
+ return (
206
+ <Text
207
+ fg={color}
208
+ width={width}
209
+ wrapText
210
+ wrapMode="word"
211
+ attributes={bold ? TextAttributes.BOLD : undefined}
212
+ >
213
+ {value}
214
+ </Text>
215
+ );
216
+ }
217
+
218
+ function SectionHeading({ label }: { label: string }) {
219
+ return (
220
+ <Box height={1}>
221
+ <Text fg={colors.textMuted} attributes={TextAttributes.BOLD}>{t(label)}</Text>
222
+ </Box>
223
+ );
224
+ }
225
+
226
+ function DiagnosticLoading({ step }: { step: number }) {
227
+ const visible = LOADING_STEPS.slice(0, Math.max(1, step));
228
+ return (
229
+ <Box flexDirection="column" gap={1}>
230
+ {visible.slice(0, -1).map((label) => (
231
+ <Text key={label} fg={colors.textDim}>{t(label)}</Text>
232
+ ))}
233
+ <Spinner label={`${t(visible.at(-1) ?? LOADING_STEPS[0])}...`} />
234
+ </Box>
235
+ );
236
+ }
237
+
238
+ /** Label plus body text, stacked instead of columned once the pane gets narrow. */
239
+ function LabeledText({ label, text: value, width, color }: {
240
+ label: string;
241
+ text: string;
242
+ width: number;
243
+ color: string;
244
+ }) {
245
+ if (width < STACK_BELOW_WIDTH) {
246
+ return (
247
+ <Box flexDirection="column" width={width}>
248
+ <Box height={1}><Text fg={colors.textMuted}>{t(label)}</Text></Box>
249
+ <Paragraph text={value} width={width} color={color} />
250
+ </Box>
251
+ );
252
+ }
253
+
254
+ return (
255
+ <Box flexDirection="row" width={width}>
256
+ <Box width={LABEL_WIDTH} flexShrink={0}>
257
+ <Text fg={colors.textMuted}>{t(label)}</Text>
258
+ </Box>
259
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
260
+ <Paragraph text={value} width={width - LABEL_WIDTH} color={color} />
261
+ </Box>
262
+ </Box>
263
+ );
264
+ }
265
+
266
+ function FindingView({ finding, evidenceById, width }: {
267
+ finding: CloudEquityDiagnosticFinding;
268
+ evidenceById: Map<string, CloudEquityDiagnosticEvidence>;
269
+ width: number;
270
+ }) {
271
+ const kindColor = findingColor(finding.kind);
272
+ const citations = finding.evidenceIds
273
+ .map((id) => evidenceById.get(id))
274
+ .filter((evidence): evidence is CloudEquityDiagnosticEvidence => !!evidence);
275
+
276
+ return (
277
+ <Box flexDirection="column" width={width}>
278
+ <Box flexDirection="row" width={width}>
279
+ <Box width={8} flexShrink={0}>
280
+ <Text fg={kindColor} attributes={TextAttributes.BOLD}>{severityLabel(finding.severity)}</Text>
281
+ </Box>
282
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
283
+ <Paragraph text={finding.title} width={width - 8} color={colors.textBright} bold />
284
+ </Box>
285
+ </Box>
286
+ <LabeledText label="Observed" text={finding.observation} width={width} color={colors.text} />
287
+ <LabeledText label="Reading" text={finding.interpretation} width={width} color={colors.textDim} />
288
+ <Box flexDirection="row" flexWrap="wrap" width={width}>
289
+ <Text fg={colors.textMuted}>{tf("{value} confidence", { value: percent(finding.confidence) })}</Text>
290
+ {citations.map((evidence) => (
291
+ <Box key={evidence.id} flexDirection="row">
292
+ <Text fg={colors.textMuted}>{" · "}</Text>
293
+ {evidence.url
294
+ ? <ExternalLinkText url={evidence.url} label={citationLabel(evidence)} color={colors.textDim} />
295
+ : <Text fg={colors.textMuted}>{citationLabel(evidence)}</Text>}
296
+ </Box>
297
+ ))}
298
+ </Box>
299
+ </Box>
300
+ );
301
+ }
302
+
303
+ function FindingSection({ heading, findings, evidenceById, width }: {
304
+ heading: string;
305
+ findings: CloudEquityDiagnosticFinding[];
306
+ evidenceById: Map<string, CloudEquityDiagnosticEvidence>;
307
+ width: number;
308
+ }) {
309
+ if (findings.length === 0) return null;
310
+ return (
311
+ <Box flexDirection="column" width={width} gap={1}>
312
+ <SectionHeading label={heading} />
313
+ {findings.map((finding) => (
314
+ <FindingView key={finding.id} finding={finding} evidenceById={evidenceById} width={width} />
315
+ ))}
316
+ </Box>
317
+ );
318
+ }
319
+
320
+ function CoverageSection({ coverage, width }: {
321
+ coverage: CloudEquityDiagnosticCoverage[];
322
+ width: number;
323
+ }) {
324
+ if (coverage.length === 0) return null;
325
+ const detailWidth = Math.max(12, width - COVERAGE_LABEL_WIDTH);
326
+
327
+ return (
328
+ <Box flexDirection="column" width={width}>
329
+ <SectionHeading label="COVERAGE" />
330
+ {coverage.map((entry) => {
331
+ const detail = [
332
+ coverageLabel(entry.status),
333
+ entry.asOf?.slice(0, 10),
334
+ providerLabel(entry.provider),
335
+ entry.note,
336
+ ]
337
+ .filter(Boolean)
338
+ .join(" · ");
339
+ return (
340
+ <Box key={`${entry.dataset}:${entry.status}`} flexDirection="row" width={width}>
341
+ <Box width={COVERAGE_LABEL_WIDTH} flexShrink={0}>
342
+ <Text fg={colors.textDim}>
343
+ {truncateToDisplayWidth(datasetLabel(entry.dataset), COVERAGE_LABEL_WIDTH - 1)}
344
+ </Text>
345
+ </Box>
346
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
347
+ <Paragraph text={detail} width={detailWidth} color={coverageColor(entry.status)} />
348
+ </Box>
349
+ </Box>
350
+ );
351
+ })}
352
+ </Box>
353
+ );
354
+ }
355
+
356
+ function ReportView({ report, width, failure, onRetry }: {
357
+ report: CloudEquityDiagnosticResponse;
358
+ width: number;
359
+ failure: DiagnosticFailure | null;
360
+ onRetry: () => void;
361
+ }) {
362
+ const evidenceById = useMemo(
363
+ () => new Map(report.evidence.map((evidence) => [evidence.id, evidence])),
364
+ [report.evidence],
365
+ );
366
+ const sorted = useMemo(() => sortFindings(report.findings), [report.findings]);
367
+ const byKind = (kind: CloudEquityDiagnosticFindingKind) => sorted.filter((finding) => finding.kind === kind);
368
+ const meta = [
369
+ report.companyName,
370
+ tf("generated {age}", { age: formatTimeAgo(report.generatedAt) }),
371
+ tf("confidence {value}", { value: percent(report.confidence) }),
372
+ ].filter(Boolean).join(" · ");
373
+
374
+ return (
375
+ <Box flexDirection="column" width={width} gap={1}>
376
+ <Box flexDirection="column" width={width}>
377
+ <Box height={1}>
378
+ <Text fg={verdictColor(report.verdict)} attributes={TextAttributes.BOLD}>
379
+ {verdictLabel(report.verdict)}
380
+ </Text>
381
+ </Box>
382
+ <Paragraph text={meta} width={width} color={colors.textMuted} />
383
+ </Box>
384
+
385
+ {failure && (
386
+ <Box flexDirection="row" gap={1} width={width}>
387
+ <Text fg={colors.warning}>{failureText(failure)}</Text>
388
+ <Button label="Retry" variant="secondary" onPress={onRetry} />
389
+ </Box>
390
+ )}
391
+
392
+ {report.status === "insufficient_data"
393
+ ? <EmptyState title="Not enough coverage to review this company yet." message={report.summary} />
394
+ : <Paragraph text={report.summary} width={width} color={colors.text} />}
395
+
396
+ <FindingSection heading="RED FLAGS" findings={byKind("red_flag")} evidenceById={evidenceById} width={width} />
397
+ <FindingSection heading="ANOMALIES" findings={byKind("anomaly")} evidenceById={evidenceById} width={width} />
398
+ <FindingSection heading="GREEN FLAGS" findings={byKind("green_flag")} evidenceById={evidenceById} width={width} />
399
+
400
+ {report.watchItems.length > 0 && (
401
+ <Box flexDirection="column" width={width}>
402
+ <SectionHeading label="WATCH ITEMS" />
403
+ {report.watchItems.map((item, index) => (
404
+ <Box key={`${index}:${item.slice(0, 24)}`} flexDirection="row" width={width}>
405
+ <Box width={2} flexShrink={0}><Text fg={colors.textMuted}>{"· "}</Text></Box>
406
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
407
+ <Paragraph text={item} width={width - 2} color={colors.text} />
408
+ </Box>
409
+ </Box>
410
+ ))}
411
+ </Box>
412
+ )}
413
+
414
+ <CoverageSection coverage={report.coverage} width={width} />
415
+ </Box>
416
+ );
417
+ }
418
+
419
+ function PreviewReportView({ report, width, onUpgrade, onPlan }: {
420
+ report: CloudEquityDiagnosticResponse;
421
+ width: number;
422
+ onUpgrade: () => void;
423
+ onPlan: () => void;
424
+ }) {
425
+ const evidenceById = useMemo(
426
+ () => new Map(report.evidence.map((evidence) => [evidence.id, evidence])),
427
+ [report.evidence],
428
+ );
429
+ const finding = report.findings[0];
430
+ const meta = [
431
+ report.companyName,
432
+ t("Free preview"),
433
+ tf("generated {age}", { age: formatTimeAgo(report.generatedAt) }),
434
+ ].filter(Boolean).join(" · ");
435
+
436
+ return (
437
+ <Box flexDirection="column" width={width} gap={1}>
438
+ <Paragraph text={meta} width={width} color={colors.textMuted} />
439
+ {finding
440
+ ? <FindingView finding={finding} evidenceById={evidenceById} width={width} />
441
+ : <EmptyState title="No preview finding is available for this company yet." />}
442
+ <Box flexDirection="column" width={width}>
443
+ <SectionHeading label="UNLOCK THE FULL DIAGNOSTIC" />
444
+ <Paragraph
445
+ text={t("See the overall verdict, every red flag, anomaly, green flag, and watch item.")}
446
+ width={width}
447
+ color={colors.text}
448
+ />
449
+ <Box flexDirection="row" marginTop={1} gap={1}>
450
+ <Button label={t("Upgrade to Pro")} onPress={onUpgrade} />
451
+ <Button label={t("Manage account")} variant="secondary" onPress={onPlan} />
452
+ </Box>
453
+ </Box>
454
+ <CoverageSection coverage={report.coverage} width={width} />
455
+ </Box>
456
+ );
457
+ }
458
+
459
+ export function EquityDiagnosticView({ focused, width }: {
460
+ focused: boolean;
461
+ width: number;
462
+ height: number;
463
+ }) {
464
+ const { symbol, exchange } = useBoundTicker();
465
+ const access = usePlanAccess();
466
+ const openUpgrade = useCloudUpgradeAction();
467
+ const openPlan = useCloudPlanAction();
468
+ const { nativePaneChrome } = useUiCapabilities();
469
+
470
+ const requestEnabled = access.emailVerified;
471
+ const { report, loading, loadingStep, failure, load } = useEquityDiagnostic(
472
+ symbol,
473
+ exchange,
474
+ requestEnabled,
475
+ );
476
+
477
+ const signInRequired = !access.signedIn || failure?.status === 401;
478
+ const verificationRequired = !signInRequired && (!access.emailVerified || failure?.status === 403);
479
+ const proRequired = !signInRequired && !verificationRequired && failure?.status === 402;
480
+ const canRefresh = !!symbol && access.hasProAccess && !signInRequired && !verificationRequired && !proRequired;
481
+
482
+ const refresh = useCallback(() => load("refresh"), [load]);
483
+ const retry = useCallback(() => load("cache-first"), [load]);
484
+
485
+ useShortcut((event) => {
486
+ if (!isPlainKey(event, "r")) return;
487
+ event.preventDefault();
488
+ event.stopPropagation();
489
+ refresh();
490
+ }, { enabled: focused && canRefresh && !loading, scope: REFRESH_SCOPE });
491
+
492
+ usePaneFooter(FOOTER_ID, () => ({
493
+ info: [
494
+ ...(loading ? [{ id: "loading", parts: [{ text: t("scanning"), tone: "muted" as const }] }] : []),
495
+ ...(failure ? [{ id: "error", parts: [{ text: failureText(failure), tone: "warning" as const }] }] : []),
496
+ ...(report?.access === "full" && report.status === "partial"
497
+ ? [{ id: "partial", parts: [{ text: t("partial"), tone: "warning" as const }] }]
498
+ : []),
499
+ ...(report?.stale ? [{ id: "stale", parts: [{ text: t("stale"), tone: "warning" as const }] }] : []),
500
+ ...(report?.cached && !report.stale ? [{ id: "cached", parts: [{ text: t("cached"), tone: "muted" as const }] }] : []),
501
+ ],
502
+ hints: canRefresh
503
+ ? [{ id: "refresh", key: "r", label: "efresh", onPress: refresh, disabled: loading }]
504
+ : [],
505
+ }), [canRefresh, failure, loading, refresh, report]);
506
+
507
+ const contentWidth = Math.max(12, width - 2);
508
+
509
+ const body = (): ReactNode => {
510
+ if (!symbol) {
511
+ return (
512
+ <EmptyState
513
+ title="No ticker selected."
514
+ hint="Move the cursor in a list pane to populate this view."
515
+ />
516
+ );
517
+ }
518
+ if (signInRequired) {
519
+ return <CloudAuthNotice message={t("Sign in to run the Equity Diagnostic.")} />;
520
+ }
521
+ if (verificationRequired) {
522
+ return <CloudAuthNotice needsVerification message={t("Verify your email to run the Equity Diagnostic.")} />;
523
+ }
524
+ if (proRequired) {
525
+ return (
526
+ <Box flexDirection="column">
527
+ <EmptyState
528
+ title="The Equity Diagnostic is part of Gloom Cloud Pro."
529
+ message="An on-demand review of one company's filings, financials, ownership, and news, with red flags, anomalies, and green flags cited back to their source."
530
+ />
531
+ <Box flexDirection="row" marginTop={1} gap={1}>
532
+ <Button label={t("Upgrade to Pro")} onPress={openUpgrade} />
533
+ <Button label={t("Manage account")} variant="secondary" onPress={openPlan} />
534
+ </Box>
535
+ </Box>
536
+ );
537
+ }
538
+ if (loading && !report) {
539
+ return <DiagnosticLoading step={loadingStep} />;
540
+ }
541
+ if (!report) {
542
+ return (
543
+ <Box flexDirection="column">
544
+ <EmptyState
545
+ title={failure ? failureText(failure) : t("No diagnostic available yet.")}
546
+ />
547
+ <Box flexDirection="row" marginTop={1}>
548
+ <Button label="Retry" variant="secondary" onPress={retry} />
549
+ </Box>
550
+ </Box>
551
+ );
552
+ }
553
+ if (report.access === "preview") {
554
+ return (
555
+ <PreviewReportView
556
+ report={report}
557
+ width={contentWidth}
558
+ onUpgrade={openUpgrade}
559
+ onPlan={openPlan}
560
+ />
561
+ );
562
+ }
563
+ return <ReportView report={report} width={contentWidth} failure={failure} onRetry={retry} />;
564
+ };
565
+
566
+ return (
567
+ <Box
568
+ flexDirection="column"
569
+ width={nativePaneChrome ? "100%" : width}
570
+ flexGrow={1}
571
+ flexBasis={0}
572
+ minHeight={0}
573
+ overflow="hidden"
574
+ >
575
+ <ScrollBox flexGrow={1} flexBasis={0} minHeight={0} scrollY focusable={false}>
576
+ <Box flexDirection="column" paddingX={1} paddingY={1} width={nativePaneChrome ? "100%" : width}>
577
+ {body()}
578
+ </Box>
579
+ </ScrollBox>
580
+ </Box>
581
+ );
582
+ }
@@ -293,6 +293,20 @@ describe("event rows", () => {
293
293
  expect(ttm?.qRevenue).toBeUndefined();
294
294
  });
295
295
 
296
+ test("omits a TTM row when a flow metric is missing from one of the last four quarters", () => {
297
+ const rows = buildEventRows(null, null, {
298
+ quarterlyStatements: [
299
+ { date: "2025-06-30", totalRevenue: 80, eps: 1 },
300
+ { date: "2025-09-30", totalRevenue: 90 },
301
+ { date: "2025-12-31", totalRevenue: 100, eps: 1.2 },
302
+ { date: "2026-03-31", totalRevenue: 110, eps: 1.3 },
303
+ ],
304
+ }, "USD");
305
+
306
+ expect(rows.find((row) => row.status === "TTM")?.annualEps).toBeUndefined();
307
+ expect(rows.find((row) => row.status === "TTM")?.annualRevenue).toBe(380);
308
+ });
309
+
296
310
  test("keeps dividends and splits in the event table without metric values", () => {
297
311
  const data = {
298
312
  symbol: "AAPL",
@@ -3,6 +3,7 @@ import { parseTickerListInput, formatTickerListInput } from "../../../tickers/li
3
3
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
4
4
  import { AnalystResearchView } from "./analyst-pane";
5
5
  import { CorporateActionsView } from "./corporate-actions-pane";
6
+ import { EquityDiagnosticView } from "./equity-diagnostic-pane";
6
7
  import { RelativeValuationPane } from "./relative-valuation-pane";
7
8
 
8
9
  function EarningsEstimatesPane(props: { focused: boolean; width: number; height: number }) {
@@ -24,6 +25,13 @@ export const researchModule: PluginModule = {
24
25
  component: AnalystResearchView,
25
26
  isVisible: ({ ticker }) => !!ticker,
26
27
  });
28
+ ctx.registerTickerResearchTab({
29
+ id: "equity-diagnostic",
30
+ name: "Diagnostic",
31
+ order: 33,
32
+ component: EquityDiagnosticView,
33
+ isVisible: ({ ticker }) => !!ticker,
34
+ });
27
35
  ctx.registerTickerResearchTab({
28
36
  id: "corporate-actions",
29
37
  name: "Events",
@@ -43,6 +51,15 @@ export const researchModule: PluginModule = {
43
51
  defaultMode: "floating",
44
52
  defaultFloatingSize: { width: 90, height: 28 },
45
53
  },
54
+ {
55
+ id: "equity-diagnostic",
56
+ name: "Equity Diagnostic",
57
+ icon: "D",
58
+ component: EquityDiagnosticView,
59
+ defaultPosition: "right",
60
+ defaultMode: "floating",
61
+ defaultFloatingSize: { width: 96, height: 30 },
62
+ },
46
63
  {
47
64
  id: "corporate-actions",
48
65
  name: "Corporate Actions",
@@ -80,6 +97,16 @@ export const researchModule: PluginModule = {
80
97
  description: "Price targets, recommendations, and recent analyst actions.",
81
98
  keywords: ["analyst", "research", "ratings", "target", "anr"],
82
99
  shortcut: "ANR",
100
+ publicShare: true,
101
+ }),
102
+ createTickerSurfacePaneTemplate({
103
+ id: "equity-diagnostic-pane",
104
+ paneId: "equity-diagnostic",
105
+ label: "Equity Diagnostic",
106
+ description: "Red flags, anomalies, green flags, and watch items for one company, with cited evidence.",
107
+ keywords: ["diagnostic", "diag", "red flags", "anomalies", "green flags", "review", "evidence"],
108
+ shortcut: "DIAG",
109
+ publicShare: true,
83
110
  }),
84
111
  createTickerSurfacePaneTemplate({
85
112
  id: "corporate-actions-pane",
@@ -88,6 +115,7 @@ export const researchModule: PluginModule = {
88
115
  description: "Dividends, splits, reported earnings, and analyst estimates.",
89
116
  keywords: ["events", "corporate", "actions", "dividend", "split", "earnings", "estimate", "revenue", "evt"],
90
117
  shortcut: "EVT",
118
+ publicShare: true,
91
119
  }),
92
120
  createTickerSurfacePaneTemplate({
93
121
  id: "earnings-estimates-pane",
@@ -96,6 +124,7 @@ export const researchModule: PluginModule = {
96
124
  description: "EPS and revenue estimates with reported earnings.",
97
125
  keywords: ["earnings", "estimates", "ee", "analyst", "eps", "revenue", "events"],
98
126
  shortcut: "EE",
127
+ publicShare: true,
99
128
  }),
100
129
  {
101
130
  id: "relative-valuation-pane",