gloomberb 0.15.7 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (524) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/src/api-client/app-attention.ts +147 -0
  4. package/src/api-client/attention.ts +26 -0
  5. package/src/api-client/awards.ts +215 -0
  6. package/src/api-client/catalysts.ts +160 -0
  7. package/src/api-client/chat.ts +137 -14
  8. package/src/api-client/company-kpis.ts +153 -0
  9. package/src/api-client/cpi.ts +52 -0
  10. package/src/api-client/credit-documents.ts +107 -0
  11. package/src/api-client/data.ts +192 -3
  12. package/src/api-client/distress.ts +164 -0
  13. package/src/api-client/doe.ts +75 -0
  14. package/src/api-client/equity-screener.ts +11 -0
  15. package/src/api-client/exposure.ts +202 -0
  16. package/src/api-client/gpu.ts +93 -0
  17. package/src/api-client/hiring.ts +110 -0
  18. package/src/api-client/index.ts +78 -3
  19. package/src/api-client/normalizers.ts +31 -1
  20. package/src/api-client/paths.ts +3 -0
  21. package/src/api-client/perps.ts +127 -0
  22. package/src/api-client/power.ts +148 -0
  23. package/src/api-client/request.ts +58 -1
  24. package/src/api-client/research-activity.ts +172 -13
  25. package/src/api-client/socket.ts +75 -27
  26. package/src/api-client/supply-chain-graph.ts +40 -0
  27. package/src/api-client/supply-chain.ts +98 -0
  28. package/src/api-client/telemetry.ts +29 -0
  29. package/src/api-client/themes.ts +28 -0
  30. package/src/api-client/types.ts +33 -1
  31. package/src/api-client/wall-experiments.ts +100 -0
  32. package/src/api-client/wall-summary.ts +49 -0
  33. package/src/api-client/web-experiments.ts +136 -0
  34. package/src/app/pane-runtime/plugin-bindings.ts +5 -0
  35. package/src/app/pane-runtime/ticker-open-runtime.ts +8 -2
  36. package/src/app/runtime/update.ts +13 -1
  37. package/src/cli/commands/system.ts +2 -0
  38. package/src/cli/desktop-pane-shot.ts +3 -4
  39. package/src/cli/pane-functions/data.ts +78 -0
  40. package/src/cli/pane-functions/financial-ratio-shot.ts +160 -0
  41. package/src/cli/pane-functions/function-help.ts +220 -8
  42. package/src/cli/pane-functions/headless.ts +4 -0
  43. package/src/cli/pane-functions/screenshot-evidence.ts +21 -0
  44. package/src/cli/pane-functions/screenshot.ts +49 -5
  45. package/src/components/chart/composite/composite-chart.tsx +15 -2
  46. package/src/components/chart/composite/format.ts +24 -11
  47. package/src/components/chart/composite/rasterizer.ts +67 -16
  48. package/src/components/chart/composite/scene.ts +53 -2
  49. package/src/components/chart/composite/session-shading.ts +3 -2
  50. package/src/components/chart/composite/text-renderer.ts +52 -1
  51. package/src/components/chart/composite/time-axis.ts +100 -30
  52. package/src/components/chart/composite/tools.ts +2 -1
  53. package/src/components/chart/composite/types.ts +4 -0
  54. package/src/components/chart-table/index.ts +1 -0
  55. package/src/components/chart-table/layout.ts +15 -0
  56. package/src/components/command-bar/action-runtime.ts +1 -0
  57. package/src/components/command-bar/commands/direct/index.ts +32 -0
  58. package/src/components/command-bar/commands/registry.ts +7 -0
  59. package/src/components/command-bar/pane-templates/items.ts +11 -0
  60. package/src/components/command-bar/pane-templates/workflow.ts +19 -4
  61. package/src/components/command-bar/routes/root/command-items.ts +6 -0
  62. package/src/components/command-bar/routes/root/provider-search.ts +2 -0
  63. package/src/components/command-bar/routes/root/results.ts +64 -0
  64. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  65. package/src/components/command-bar/routes/ticker-search/results.ts +89 -24
  66. package/src/components/command-bar/routes/ticker-search/route.ts +2 -0
  67. package/src/components/command-bar/surface/index.tsx +15 -0
  68. package/src/components/command-bar/view-model.ts +3 -0
  69. package/src/components/command-bar/workflow/collection-actions.ts +9 -7
  70. package/src/components/command-bar/workflow/ops.ts +19 -0
  71. package/src/components/command-bar/workflow/tickers.ts +5 -0
  72. package/src/components/company-logo.tsx +15 -2
  73. package/src/components/data-table/table-pane.ts +2 -1
  74. package/src/components/index.ts +1 -0
  75. package/src/components/layout/detached-pane-shell.tsx +12 -1
  76. package/src/components/layout/missing-pane.tsx +87 -23
  77. package/src/components/layout/pane/footer/index.tsx +64 -18
  78. package/src/components/layout/pane/footer/model.ts +85 -0
  79. package/src/components/layout/pane/sidebar.tsx +19 -1
  80. package/src/components/layout/shell/index.tsx +38 -28
  81. package/src/components/layout/shell/pane/management-shortcuts.ts +13 -1
  82. package/src/components/layout/status-bar.tsx +93 -28
  83. package/src/components/layout/status-widget-space.ts +72 -0
  84. package/src/components/onboarding/first-run-workspace.ts +8 -2
  85. package/src/components/onboarding/onboarding-wizard.tsx +35 -16
  86. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -5
  87. package/src/components/onboarding/wizard-model.ts +13 -5
  88. package/src/components/onboarding/wizard-positions.ts +56 -7
  89. package/src/components/price-performance.tsx +31 -34
  90. package/src/components/ui/data-table/index.tsx +11 -8
  91. package/src/components/ui/display.tsx +15 -6
  92. package/src/components/ui/fields.tsx +5 -0
  93. package/src/components/ui/figure.tsx +94 -0
  94. package/src/components/ui/header-case.ts +25 -3
  95. package/src/components/ui/icon.tsx +3 -1
  96. package/src/components/ui/message-composer.tsx +8 -1
  97. package/src/components/ui/query-bar.tsx +8 -1
  98. package/src/components/ui/ratio-bar.tsx +84 -0
  99. package/src/components/ui/toggle.tsx +3 -1
  100. package/src/components/use-scrollbox-layout.ts +17 -0
  101. package/src/core/state/app/layout.ts +35 -1
  102. package/src/core/state/app/state.ts +15 -0
  103. package/src/core/state/app/types.ts +1 -0
  104. package/src/data/config/store/normalize.ts +24 -0
  105. package/src/i18n/es.ts +2 -0
  106. package/src/i18n/ja.ts +2 -0
  107. package/src/i18n/ko.ts +2 -0
  108. package/src/i18n/zh-cn.ts +2 -0
  109. package/src/i18n/zh-tw.ts +2 -0
  110. package/src/layout-marketplace/pane-imagery.ts +1 -0
  111. package/src/market-data/coordinator/entries.ts +13 -0
  112. package/src/market-data/coordinator/financials.ts +33 -8
  113. package/src/market-data/market/freshness.ts +16 -5
  114. package/src/market-data/market/trading-sessions.ts +65 -1
  115. package/src/market-data/published-cn-sessions.ts +25 -0
  116. package/src/market-data/published-nse-sessions.ts +20 -0
  117. package/src/news/aggregator.ts +4 -0
  118. package/src/plugins/builtin/account-management/model.ts +6 -1
  119. package/src/plugins/builtin/account-management/pane.tsx +36 -25
  120. package/src/plugins/builtin/alerts/catalysts.ts +77 -0
  121. package/src/plugins/builtin/alerts/events-pane.tsx +3 -0
  122. package/src/plugins/builtin/alerts/events.ts +10 -4
  123. package/src/plugins/builtin/alerts/history-pane.tsx +1 -1
  124. package/src/plugins/builtin/alerts/history.ts +1 -0
  125. package/src/plugins/builtin/alerts/index.tsx +6 -0
  126. package/src/plugins/builtin/alerts/research-builder.ts +10 -3
  127. package/src/plugins/builtin/alerts/research-rules.ts +15 -1
  128. package/src/plugins/builtin/analytics/risk-client.ts +15 -3
  129. package/src/plugins/builtin/analytics/risk-headless.ts +123 -18
  130. package/src/plugins/builtin/attention/client.ts +80 -0
  131. package/src/plugins/builtin/attention/evidence.ts +30 -0
  132. package/src/plugins/builtin/attention/headless.ts +33 -0
  133. package/src/plugins/builtin/attention/index.tsx +18 -0
  134. package/src/plugins/builtin/attention/model.ts +59 -0
  135. package/src/plugins/builtin/attention/pane.tsx +211 -0
  136. package/src/plugins/builtin/attention/trending.tsx +59 -0
  137. package/src/plugins/builtin/awards/alerts.ts +32 -0
  138. package/src/plugins/builtin/awards/client.ts +82 -0
  139. package/src/plugins/builtin/awards/detail.tsx +107 -0
  140. package/src/plugins/builtin/awards/evidence.ts +36 -0
  141. package/src/plugins/builtin/awards/headless.ts +55 -0
  142. package/src/plugins/builtin/awards/index.tsx +20 -0
  143. package/src/plugins/builtin/awards/model.ts +42 -0
  144. package/src/plugins/builtin/awards/pane.tsx +263 -0
  145. package/src/plugins/builtin/catalysts/client.ts +28 -0
  146. package/src/plugins/builtin/catalysts/detail.tsx +88 -0
  147. package/src/plugins/builtin/catalysts/evidence.ts +47 -0
  148. package/src/plugins/builtin/catalysts/headless.ts +42 -0
  149. package/src/plugins/builtin/catalysts/history.ts +26 -0
  150. package/src/plugins/builtin/catalysts/index.tsx +29 -0
  151. package/src/plugins/builtin/catalysts/model.ts +135 -0
  152. package/src/plugins/builtin/catalysts/pane.tsx +159 -0
  153. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  154. package/src/plugins/builtin/chart-composer/headless-schema.ts +6 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +8 -1
  156. package/src/plugins/builtin/chart-composer/pane.tsx +28 -1
  157. package/src/plugins/builtin/chat/attachments/desktop.tsx +610 -0
  158. package/src/plugins/builtin/chat/attachments/files.ts +45 -0
  159. package/src/plugins/builtin/chat/attachments/model.ts +164 -0
  160. package/src/plugins/builtin/chat/attachments/terminal.tsx +72 -0
  161. package/src/plugins/builtin/chat/attachments/transfer.ts +165 -0
  162. package/src/plugins/builtin/chat/content/channel-navigation.ts +13 -12
  163. package/src/plugins/builtin/chat/content/composer-runtime.ts +25 -4
  164. package/src/plugins/builtin/chat/content/composer.tsx +46 -4
  165. package/src/plugins/builtin/chat/content/footer.tsx +46 -8
  166. package/src/plugins/builtin/chat/content/index.tsx +263 -62
  167. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -2
  168. package/src/plugins/builtin/chat/content/open-targets.ts +17 -3
  169. package/src/plugins/builtin/chat/content/scroll.ts +7 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +36 -11
  171. package/src/plugins/builtin/chat/content/snapshot.ts +3 -0
  172. package/src/plugins/builtin/chat/content/transcript.tsx +17 -1
  173. package/src/plugins/builtin/chat/content/types.ts +7 -0
  174. package/src/plugins/builtin/chat/controller/attachments.ts +213 -0
  175. package/src/plugins/builtin/chat/controller/connections.ts +3 -1
  176. package/src/plugins/builtin/chat/controller/index.ts +114 -2
  177. package/src/plugins/builtin/chat/controller/lifecycle.ts +9 -0
  178. package/src/plugins/builtin/chat/controller/messages.ts +35 -2
  179. package/src/plugins/builtin/chat/controller/send.ts +128 -25
  180. package/src/plugins/builtin/chat/controller/state.ts +12 -1
  181. package/src/plugins/builtin/chat/controller/utils.ts +7 -5
  182. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  183. package/src/plugins/builtin/chat/index.tsx +21 -0
  184. package/src/plugins/builtin/chat/layout.ts +9 -4
  185. package/src/plugins/builtin/chat/message/desktop.tsx +55 -20
  186. package/src/plugins/builtin/chat/message/header.tsx +30 -5
  187. package/src/plugins/builtin/chat/message/inline-tokens.tsx +9 -0
  188. package/src/plugins/builtin/chat/message/pending-send.ts +35 -0
  189. package/src/plugins/builtin/chat/message/profile-popover.tsx +5 -1
  190. package/src/plugins/builtin/chat/message/render-state.ts +12 -2
  191. package/src/plugins/builtin/chat/message/terminal.tsx +51 -4
  192. package/src/plugins/builtin/chat/message/types.ts +6 -1
  193. package/src/plugins/builtin/chat/pane-state.ts +59 -1
  194. package/src/plugins/builtin/chat/profile-popover.ts +50 -1
  195. package/src/plugins/builtin/chat/sidebar.tsx +8 -3
  196. package/src/plugins/builtin/chat/status-widget.tsx +29 -12
  197. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +143 -0
  198. package/src/plugins/builtin/chat/unread-inbox.ts +91 -0
  199. package/src/plugins/builtin/clinical-trials/client.ts +237 -0
  200. package/src/plugins/builtin/clinical-trials/index.tsx +74 -0
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +225 -0
  202. package/src/plugins/builtin/clinical-trials/types.ts +2 -0
  203. package/src/plugins/builtin/cloud/askg/controller.ts +37 -6
  204. package/src/plugins/builtin/cloud/askg/executor.ts +59 -44
  205. package/src/plugins/builtin/cloud/askg/host.ts +8 -1
  206. package/src/plugins/builtin/cloud/askg/manifest.ts +29 -2
  207. package/src/plugins/builtin/cloud/askg/model.ts +47 -13
  208. package/src/plugins/builtin/cloud/askg/notes.ts +100 -0
  209. package/src/plugins/builtin/cloud/askg/pane.tsx +50 -14
  210. package/src/plugins/builtin/cloud/askg/protocol.ts +39 -1
  211. package/src/plugins/builtin/cloud/askg/result-budget.ts +156 -0
  212. package/src/plugins/builtin/cloud/askg/user-data.ts +89 -0
  213. package/src/plugins/builtin/cloud/auth-actions.tsx +55 -31
  214. package/src/plugins/builtin/cloud/plugin.tsx +4 -1
  215. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -1
  216. package/src/plugins/builtin/cloud/team/pane.tsx +3 -2
  217. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +2 -2
  218. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  219. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +2 -2
  220. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +202 -0
  221. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  222. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +29 -3
  223. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +1 -1
  224. package/src/plugins/builtin/cloud-tweets/table.tsx +1 -1
  225. package/src/plugins/builtin/comment-letters/client.ts +342 -0
  226. package/src/plugins/builtin/comment-letters/index.tsx +74 -0
  227. package/src/plugins/builtin/comment-letters/pane.tsx +295 -0
  228. package/src/plugins/builtin/comment-letters/types.ts +43 -0
  229. package/src/plugins/builtin/company-attention/app-rank.tsx +79 -0
  230. package/src/plugins/builtin/company-attention/client.ts +69 -0
  231. package/src/plugins/builtin/company-attention/evidence.ts +45 -0
  232. package/src/plugins/builtin/company-attention/headless.ts +47 -0
  233. package/src/plugins/builtin/company-attention/index.tsx +30 -0
  234. package/src/plugins/builtin/company-attention/model.ts +129 -0
  235. package/src/plugins/builtin/company-attention/pane.tsx +183 -0
  236. package/src/plugins/builtin/company-kpis/client.ts +92 -0
  237. package/src/plugins/builtin/company-kpis/headless.ts +32 -0
  238. package/src/plugins/builtin/company-kpis/index.tsx +26 -0
  239. package/src/plugins/builtin/company-kpis/model.ts +124 -0
  240. package/src/plugins/builtin/company-kpis/pane.tsx +226 -0
  241. package/src/plugins/builtin/company-kpis/related.ts +11 -0
  242. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +44 -0
  243. package/src/plugins/builtin/composite-plugins.ts +34 -0
  244. package/src/plugins/builtin/correlation/history.ts +4 -1
  245. package/src/plugins/builtin/correlation/index.tsx +139 -80
  246. package/src/plugins/builtin/correlation/matrix/pair-chart.tsx +48 -0
  247. package/src/plugins/builtin/correlation/matrix/selection.ts +77 -0
  248. package/src/plugins/builtin/correlation/relationship/model.ts +2 -2
  249. package/src/plugins/builtin/cpi/client.ts +66 -0
  250. package/src/plugins/builtin/cpi/headless.ts +78 -0
  251. package/src/plugins/builtin/cpi/index.tsx +31 -0
  252. package/src/plugins/builtin/cpi/model.ts +198 -0
  253. package/src/plugins/builtin/cpi/pane.tsx +157 -0
  254. package/src/plugins/builtin/credit-documents/client.ts +99 -0
  255. package/src/plugins/builtin/credit-documents/detail.tsx +109 -0
  256. package/src/plugins/builtin/credit-documents/evidence.ts +64 -0
  257. package/src/plugins/builtin/credit-documents/gauge.tsx +55 -0
  258. package/src/plugins/builtin/credit-documents/headless.ts +40 -0
  259. package/src/plugins/builtin/credit-documents/index.tsx +19 -0
  260. package/src/plugins/builtin/credit-documents/model.ts +336 -0
  261. package/src/plugins/builtin/credit-documents/pane.tsx +185 -0
  262. package/src/plugins/builtin/custom-view/view-spec.ts +14 -2
  263. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -0
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  265. package/src/plugins/builtin/doe/client.ts +73 -0
  266. package/src/plugins/builtin/doe/headless.ts +93 -0
  267. package/src/plugins/builtin/doe/index.tsx +41 -0
  268. package/src/plugins/builtin/doe/model.ts +215 -0
  269. package/src/plugins/builtin/doe/pane.tsx +148 -0
  270. package/src/plugins/builtin/doe/range-bar.tsx +45 -0
  271. package/src/plugins/builtin/earnings/board.tsx +3 -0
  272. package/src/plugins/builtin/earnings/index.tsx +2 -0
  273. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -1
  274. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +7 -7
  275. package/src/plugins/builtin/earnings-ripple/client.ts +60 -0
  276. package/src/plugins/builtin/earnings-ripple/headless.ts +35 -0
  277. package/src/plugins/builtin/earnings-ripple/index.tsx +35 -0
  278. package/src/plugins/builtin/earnings-ripple/model.ts +98 -0
  279. package/src/plugins/builtin/earnings-ripple/pane.tsx +103 -0
  280. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  281. package/src/plugins/builtin/equity-screener/criterion-editor.tsx +1 -1
  282. package/src/plugins/builtin/equity-screener/model.ts +74 -9
  283. package/src/plugins/builtin/equity-screener/pane.tsx +2 -0
  284. package/src/plugins/builtin/estimate-revisions/pane.tsx +3 -0
  285. package/src/plugins/builtin/executives/pane.tsx +7 -5
  286. package/src/plugins/builtin/exposure/charts.tsx +66 -0
  287. package/src/plugins/builtin/exposure/client.ts +53 -0
  288. package/src/plugins/builtin/exposure/drivers.ts +29 -0
  289. package/src/plugins/builtin/exposure/editor.tsx +61 -0
  290. package/src/plugins/builtin/exposure/evidence.ts +39 -0
  291. package/src/plugins/builtin/exposure/headless.ts +51 -0
  292. package/src/plugins/builtin/exposure/holdings.ts +56 -0
  293. package/src/plugins/builtin/exposure/index.tsx +13 -0
  294. package/src/plugins/builtin/exposure/model.ts +104 -0
  295. package/src/plugins/builtin/exposure/pane.tsx +247 -0
  296. package/src/plugins/builtin/fear-greed/charts.tsx +1 -1
  297. package/src/plugins/builtin/fear-greed/pane.tsx +2 -9
  298. package/src/plugins/builtin/filing-events/pane.tsx +2 -1
  299. package/src/plugins/builtin/gpu/board.tsx +163 -0
  300. package/src/plugins/builtin/gpu/changes.tsx +94 -0
  301. package/src/plugins/builtin/gpu/client.ts +130 -0
  302. package/src/plugins/builtin/gpu/equities.tsx +112 -0
  303. package/src/plugins/builtin/gpu/headless.ts +67 -0
  304. package/src/plugins/builtin/gpu/history.tsx +140 -0
  305. package/src/plugins/builtin/gpu/index.tsx +18 -0
  306. package/src/plugins/builtin/gpu/ladder.tsx +127 -0
  307. package/src/plugins/builtin/gpu/model.ts +317 -0
  308. package/src/plugins/builtin/gpu/pane.tsx +63 -0
  309. package/src/plugins/builtin/help/index.tsx +19 -2
  310. package/src/plugins/builtin/holders/pane.tsx +5 -9
  311. package/src/plugins/builtin/insider/detail.tsx +119 -0
  312. package/src/plugins/builtin/insider/display.ts +221 -0
  313. package/src/plugins/builtin/insider/headless.ts +24 -3
  314. package/src/plugins/builtin/insider/index.tsx +206 -108
  315. package/src/plugins/builtin/insider/insider-data.ts +31 -3
  316. package/src/plugins/builtin/insider/model.ts +53 -4
  317. package/src/plugins/builtin/insider/table-model.ts +226 -0
  318. package/src/plugins/builtin/insider/table.tsx +79 -0
  319. package/src/plugins/builtin/iv-history/screen-pane.tsx +1 -1
  320. package/src/plugins/builtin/jobs/pane.tsx +14 -7
  321. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  322. package/src/plugins/builtin/macro-day/client.ts +50 -0
  323. package/src/plugins/builtin/macro-day/headless.ts +51 -0
  324. package/src/plugins/builtin/macro-day/index.tsx +21 -0
  325. package/src/plugins/builtin/macro-day/model.ts +137 -0
  326. package/src/plugins/builtin/macro-day/pane.tsx +118 -0
  327. package/src/plugins/builtin/macro-day/releases.ts +58 -0
  328. package/src/plugins/builtin/market-halts/model.ts +30 -17
  329. package/src/plugins/builtin/market-halts/pane.tsx +15 -21
  330. package/src/plugins/builtin/market-heatmap/data.ts +1 -1
  331. package/src/plugins/builtin/market-heatmap/index.tsx +198 -50
  332. package/src/plugins/builtin/market-heatmap/portfolio.ts +220 -0
  333. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  334. package/src/plugins/builtin/market-movers/session-body.tsx +5 -2
  335. package/src/plugins/builtin/mna/distress/client.ts +326 -0
  336. package/src/plugins/builtin/mna/distress/detail.tsx +247 -0
  337. package/src/plugins/builtin/mna/distress/headless.ts +257 -0
  338. package/src/plugins/builtin/mna/distress/model.ts +427 -0
  339. package/src/plugins/builtin/mna/distress/pane.tsx +606 -0
  340. package/src/plugins/builtin/mna/index.tsx +15 -0
  341. package/src/plugins/builtin/mna/pane.tsx +42 -1
  342. package/src/plugins/builtin/news/index.tsx +62 -16
  343. package/src/plugins/builtin/news/wire/filter-articles.ts +54 -0
  344. package/src/plugins/builtin/news/wire/index.tsx +37 -3
  345. package/src/plugins/builtin/news/wire/industry-pane.tsx +29 -13
  346. package/src/plugins/builtin/news/wire/mutes.ts +253 -0
  347. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +117 -0
  348. package/src/plugins/builtin/news/wire/news/footer.ts +4 -0
  349. package/src/plugins/builtin/news/wire/news/list-search.ts +34 -0
  350. package/src/plugins/builtin/news/wire/news/pop-out.tsx +158 -0
  351. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +64 -19
  352. package/src/plugins/builtin/openfda/client.ts +300 -0
  353. package/src/plugins/builtin/openfda/index.tsx +71 -0
  354. package/src/plugins/builtin/openfda/pane.tsx +382 -0
  355. package/src/plugins/builtin/openfda/types.ts +76 -0
  356. package/src/plugins/builtin/options/footer.ts +1 -0
  357. package/src/plugins/builtin/options/view.tsx +1 -1
  358. package/src/plugins/builtin/options-calculator/pane.tsx +0 -1
  359. package/src/plugins/builtin/options-positioning/index.tsx +4 -2
  360. package/src/plugins/builtin/options-positioning/pane.tsx +1 -1
  361. package/src/plugins/builtin/pe-band/client.ts +61 -0
  362. package/src/plugins/builtin/pe-band/headless.ts +51 -0
  363. package/src/plugins/builtin/pe-band/index.tsx +20 -0
  364. package/src/plugins/builtin/pe-band/model.ts +267 -0
  365. package/src/plugins/builtin/pe-band/pane.tsx +159 -0
  366. package/src/plugins/builtin/perps/client.ts +66 -0
  367. package/src/plugins/builtin/perps/equity-row.tsx +28 -0
  368. package/src/plugins/builtin/perps/headless.ts +34 -0
  369. package/src/plugins/builtin/perps/index.tsx +14 -0
  370. package/src/plugins/builtin/perps/model.ts +58 -0
  371. package/src/plugins/builtin/perps/pane.tsx +185 -0
  372. package/src/plugins/builtin/plugin-marketplace/pane.tsx +0 -1
  373. package/src/plugins/builtin/portfolio-list/cli/render.ts +177 -87
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -20
  375. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -2
  376. package/src/plugins/builtin/portfolio-list/headless.ts +237 -0
  377. package/src/plugins/builtin/portfolio-list/index.tsx +2 -0
  378. package/src/plugins/builtin/portfolio-list/mutations.ts +70 -1
  379. package/src/plugins/builtin/portfolio-list/pane/data.ts +17 -0
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +88 -5
  381. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +85 -0
  382. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +18 -12
  383. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -0
  384. package/src/plugins/builtin/power/client.ts +100 -0
  385. package/src/plugins/builtin/power/detail.tsx +82 -0
  386. package/src/plugins/builtin/power/evidence.ts +56 -0
  387. package/src/plugins/builtin/power/generation.tsx +37 -0
  388. package/src/plugins/builtin/power/headless.ts +43 -0
  389. package/src/plugins/builtin/power/index.tsx +16 -0
  390. package/src/plugins/builtin/power/model.ts +89 -0
  391. package/src/plugins/builtin/power/pane.tsx +209 -0
  392. package/src/plugins/builtin/power/query.ts +25 -0
  393. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -3
  394. package/src/plugins/builtin/research/analyst-pane.tsx +1 -1
  395. package/src/plugins/builtin/research/corporate-actions-pane.tsx +13 -4
  396. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +11 -9
  397. package/src/plugins/builtin/research-search/pane.tsx +21 -7
  398. package/src/plugins/builtin/revenue-breakdown/pane.tsx +1 -1
  399. package/src/plugins/builtin/reverse-dcf/client.ts +35 -0
  400. package/src/plugins/builtin/reverse-dcf/headless.ts +44 -0
  401. package/src/plugins/builtin/reverse-dcf/index.tsx +20 -0
  402. package/src/plugins/builtin/reverse-dcf/model.ts +87 -0
  403. package/src/plugins/builtin/reverse-dcf/pane.tsx +107 -0
  404. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  405. package/src/plugins/builtin/scanner/denied.tsx +4 -2
  406. package/src/plugins/builtin/scanner/feed.ts +1 -0
  407. package/src/plugins/builtin/scanner/flow-pane.tsx +3 -2
  408. package/src/plugins/builtin/scanner/hilo-pane.tsx +1 -1
  409. package/src/plugins/builtin/seasonality/client.ts +4 -1
  410. package/src/plugins/builtin/sec/command-bar-search.ts +217 -0
  411. package/src/plugins/builtin/sec/forms.ts +54 -0
  412. package/src/plugins/builtin/sec/headless.ts +31 -5
  413. package/src/plugins/builtin/sec/index.tsx +67 -11
  414. package/src/plugins/builtin/sec/model.ts +35 -5
  415. package/src/plugins/builtin/sectors/headless.ts +1 -0
  416. package/src/plugins/builtin/sectors/index.tsx +72 -26
  417. package/src/plugins/builtin/shared/cloud-upgrade.ts +143 -39
  418. package/src/plugins/builtin/shared/pro-feature-wall.tsx +8 -4
  419. package/src/plugins/builtin/shared/research-cells.tsx +107 -0
  420. package/src/plugins/builtin/shared/series-list-detail.tsx +5 -1
  421. package/src/plugins/builtin/shared/ticker-request.ts +1 -1
  422. package/src/plugins/builtin/shared/wall-teaser-catalog.ts +80 -0
  423. package/src/plugins/builtin/shared/wall-teaser.tsx +192 -0
  424. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +43 -0
  425. package/src/plugins/builtin/short-interest/index.tsx +31 -0
  426. package/src/plugins/builtin/short-interest/watch-client.ts +70 -0
  427. package/src/plugins/builtin/short-interest/watch-headless.ts +49 -0
  428. package/src/plugins/builtin/short-interest/watch-model.ts +185 -0
  429. package/src/plugins/builtin/short-interest/watch-pane.tsx +169 -0
  430. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  431. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  432. package/src/plugins/builtin/supply-chain/client.ts +104 -0
  433. package/src/plugins/builtin/supply-chain/disclosure-quote.tsx +17 -0
  434. package/src/plugins/builtin/supply-chain/evidence.ts +88 -0
  435. package/src/plugins/builtin/supply-chain/flow-layout.ts +133 -0
  436. package/src/plugins/builtin/supply-chain/flow.tsx +323 -0
  437. package/src/plugins/builtin/supply-chain/graph-client.ts +83 -0
  438. package/src/plugins/builtin/supply-chain/graph-evidence.ts +25 -0
  439. package/src/plugins/builtin/supply-chain/graph-model.ts +45 -0
  440. package/src/plugins/builtin/supply-chain/graph-pane.tsx +218 -0
  441. package/src/plugins/builtin/supply-chain/graph.tsx +163 -0
  442. package/src/plugins/builtin/supply-chain/headless.ts +105 -0
  443. package/src/plugins/builtin/supply-chain/index.tsx +17 -0
  444. package/src/plugins/builtin/supply-chain/model.ts +145 -0
  445. package/src/plugins/builtin/supply-chain/pane.tsx +276 -0
  446. package/src/plugins/builtin/supply-chain/row-evidence.tsx +96 -0
  447. package/src/plugins/builtin/supply-chain/trust.ts +34 -0
  448. package/src/plugins/builtin/themes/client.ts +39 -0
  449. package/src/plugins/builtin/themes/headless.ts +38 -0
  450. package/src/plugins/builtin/themes/model.ts +92 -0
  451. package/src/plugins/builtin/themes/pane.tsx +125 -0
  452. package/src/plugins/builtin/thirteenf/pane.tsx +6 -15
  453. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -6
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +201 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +327 -0
  456. package/src/plugins/builtin/ticker-detail/financials/period-end-history.ts +5 -2
  457. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +60 -12
  458. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +2 -1
  459. package/src/plugins/builtin/ticker-detail/headless.ts +7 -2
  460. package/src/plugins/builtin/ticker-detail/index.tsx +22 -0
  461. package/src/plugins/builtin/ticker-detail/overview/components.tsx +53 -30
  462. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -0
  463. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +112 -47
  464. package/src/plugins/builtin/ticker-detail/pane.tsx +50 -6
  465. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +21 -42
  466. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +3 -0
  467. package/src/plugins/builtin/time-sales/pane.tsx +1 -1
  468. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  469. package/src/plugins/catalog-browser.ts +40 -0
  470. package/src/plugins/catalog-manifest.ts +3 -0
  471. package/src/plugins/catalog-ui.ts +6 -0
  472. package/src/plugins/current-target.ts +0 -9
  473. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  474. package/src/renderers/browser/cloud-transport.ts +66 -1
  475. package/src/renderers/browser/main.tsx +2 -0
  476. package/src/renderers/dom/data-table/row.tsx +2 -0
  477. package/src/renderers/dom/desktop/controls.tsx +38 -25
  478. package/src/renderers/dom/desktop/icons.tsx +8 -0
  479. package/src/renderers/dom/host/chart-surface.tsx +36 -2
  480. package/src/renderers/dom/host/focus-scope.ts +11 -1
  481. package/src/renderers/dom/styles.css +28 -0
  482. package/src/renderers/electrobun/view/backend-rpc.ts +3 -2
  483. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  484. package/src/renderers/electrobun/view/http-fetch.ts +2 -0
  485. package/src/renderers/electrobun/view/main.tsx +3 -1
  486. package/src/renderers/electrobun/view/rpc-timeout.ts +59 -0
  487. package/src/renderers/opentui/start.tsx +3 -0
  488. package/src/shares/api.ts +3 -1
  489. package/src/sources/gloomberb-cloud/index.ts +47 -13
  490. package/src/sources/history-result.ts +1 -0
  491. package/src/sources/history-retention.ts +16 -0
  492. package/src/sources/provider-router/history.ts +4 -2
  493. package/src/sources/sec-edgar/identity.ts +36 -0
  494. package/src/sources/sec-edgar.ts +1 -34
  495. package/src/state/app/context/index.tsx +8 -0
  496. package/src/sync/core-contributors.ts +20 -2
  497. package/src/telemetry/attention-counts.ts +164 -0
  498. package/src/telemetry/live-config.ts +7 -0
  499. package/src/test-support/rpc-loopback.ts +7 -3
  500. package/src/test-support/shell-pane-keys.tsx +34 -0
  501. package/src/tickers/instrument-kind.ts +12 -6
  502. package/src/tickers/open-target.ts +1 -1
  503. package/src/tickers/search/asset-classes.ts +91 -0
  504. package/src/tickers/search/index.ts +62 -15
  505. package/src/tickers/search/ranking.ts +22 -3
  506. package/src/tickers/search/upsert.ts +4 -1
  507. package/src/time-series/live-bars.ts +8 -3
  508. package/src/time-series/resolve.ts +224 -17
  509. package/src/time-series/spec.ts +1 -0
  510. package/src/time-series/types.ts +9 -1
  511. package/src/types/config.ts +23 -1
  512. package/src/types/data-provider.ts +2 -0
  513. package/src/types/headless.ts +17 -0
  514. package/src/types/price-history.ts +2 -0
  515. package/src/ui/host.tsx +6 -0
  516. package/src/utils/double-escape-close.ts +20 -0
  517. package/src/utils/exchanges.ts +1 -0
  518. package/src/utils/html-entities.ts +15 -0
  519. package/src/utils/http-proxy-response.ts +44 -7
  520. package/src/utils/plugin-proxy-hosts.json +3 -0
  521. package/src/utils/price-history.ts +35 -1
  522. package/src/utils/sec.ts +21 -2
  523. package/src/utils/text-wrap.ts +13 -8
  524. package/src/utils/zoned-date-time.ts +67 -0
@@ -0,0 +1,129 @@
1
+ import type { DataTableColumn, StatItem } from "../../../components";
2
+ import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
3
+ import type { HiringBoard, HiringPayload, HiringSummary } from "../../../api-client/hiring";
4
+ import type { AppAttentionPayload, AppCompany } from "../../../api-client/app-attention";
5
+ import { humanLabel } from "../shared/research-cells";
6
+
7
+ export type AttentionKind = "hiring" | "apps";
8
+ export type AttentionTab = "table" | "chart" | "mix" | "peers" | "evidence";
9
+ export interface AttentionRow {
10
+ id: string;
11
+ values: Record<string, string | number | null>;
12
+ symbol?: string;
13
+ date?: string;
14
+ url?: string | null;
15
+ details?: Array<{ label: string; value: string }>;
16
+ app?: AppFocus;
17
+ }
18
+ export interface AppFocus { store: "app-store" | "google-play"; appId: string; name: string; country: string; chart: "free" | "paid" | "grossing" | "unranked" }
19
+ interface AttentionSection {
20
+ columns: DataTableColumn[];
21
+ rows: AttentionRow[];
22
+ empty: string;
23
+ }
24
+ export interface AttentionModel {
25
+ asOf: string | null;
26
+ preview: boolean;
27
+ locked: number;
28
+ notices: string[];
29
+ figures: StatItem[];
30
+ sections: Record<AttentionTab, AttentionSection>;
31
+ chart: { label: string; unit: string; points: Array<{ date: string; value: number | null }> };
32
+ }
33
+ export const count = (value: number | null | undefined) => value == null ? "--" : value.toLocaleString("en-US", { maximumFractionDigits: 0 });
34
+ const signed = (value: number | null | undefined, decimals = 0) => value == null ? "--" : `${value > 0 ? "+" : ""}${value.toLocaleString("en-US", { maximumFractionDigits: decimals })}`;
35
+ const percent = (value: number | null | undefined) => value == null ? "--" : `${(value * 100).toFixed(1)}%`;
36
+ const column = (id: string, label: string, width: number, numeric = false, flex = false): DataTableColumn => ({ id, label, width, align: numeric ? "right" : "left", ...(flex ? { flexGrow: 1 } : {}) });
37
+ const roleFamily = (value: string | null) => value === "data_ai" || value === "data ai" ? "Data / AI" : value ? humanLabel(value) : null;
38
+ const COMPANY_COLUMNS = [column("symbol", "Ticker", 12), column("name", "Company", 24, false, true), column("open", "Open roles", 12, true), column("net", "Net / week", 12, true), column("remote", "Remote %", 10, true), column("z", "Z-score", 10, true), column("signal", "Signal", 12), column("date", "As of", 12)];
39
+ const HISTORY_COLUMNS = [column("date", "Week", 12), column("open", "Open roles", 12, true), column("added", "Added", 9, true), column("removed", "Removed", 9, true), column("net", "Net", 9, true), column("change", "Change %", 11, true), column("remote", "Remote %", 10, true), column("coverage", "Capture", 13, false, true)];
40
+ const companyRow = (row: HiringSummary): AttentionRow => ({ id: row.symbol, symbol: row.symbol, values: { symbol: row.symbol, name: row.name, open: row.latest?.openCount ?? null, net: row.latest?.netChange ?? null, remote: row.latest?.remoteShare == null ? null : row.latest.remoteShare * 100, z: row.zScore, signal: humanLabel(row.signal ?? row.status), date: row.latest?.observedAt.slice(0, 10) ?? null } });
41
+
42
+ export function hiringModel(data: HiringBoard | HiringPayload, mix = "functions"): AttentionModel {
43
+ const detail = "series" in data ? data : null;
44
+ const companies = "companies" in data ? data.companies : [data];
45
+ const history = detail?.series ?? [];
46
+ const latest = detail?.latest;
47
+ const mixKey = ["functions", "seniority", "countries", "locations", "signals"].includes(mix) ? mix as "functions" | "seniority" | "countries" | "locations" | "signals" : "functions";
48
+ const rows = mixKey === "signals" ? (detail?.signals ?? []).map((row, i): AttentionRow => ({ id: `${row.type}:${i}`, url: row.evidenceUrls[0], values: { label: row.label, count: row.value, share: row.confidence * 100, date: row.asOf.slice(0, 10) }, details: [{ label: "Observed", value: row.asOf }, { label: "Confidence", value: percent(row.confidence) }, { label: "Location", value: row.location ?? "--" }] }))
49
+ : (detail?.[mixKey] ?? []).map((row): AttentionRow => ({ id: row.id, values: { label: mixKey === "functions" ? roleFamily(row.label) : mixKey === "seniority" ? humanLabel(row.label) : row.label, count: row.count, share: row.share * 100 } }));
50
+ const model: AttentionModel = {
51
+ asOf: latest?.observedAt ?? data.generatedAt,
52
+ preview: data.preview,
53
+ locked: typeof data.locked === "number" ? data.locked : Object.values(data.locked).reduce((a, b) => a + b, 0),
54
+ // How counts compare is methodology (docs/hiring-app-attention.md); the footer keeps only what is wrong with this capture.
55
+ notices: detail ? [...(latest?.completeness !== "complete" ? ["The latest capture has incomplete or unknown coverage."] : []), ...(detail.status === "stale" ? ["The latest hiring capture is stale."] : [])] : [],
56
+ // A figure the first capture cannot give yet (a weekly change, a z-score) is left out, not shown as a dash.
57
+ figures: latest ? [
58
+ { label: "Open roles", value: count(latest.openCount) },
59
+ ...(latest.netChange != null ? [{ label: "Net / week", value: signed(latest.netChange), tone: latest.netChange === 0 ? "muted" as const : latest.netChange > 0 ? "positive" as const : "negative" as const }] : []),
60
+ ...(latest.remoteShare != null ? [{ label: "Remote", value: percent(latest.remoteShare), detail: "known work modes" }] : []),
61
+ ...(detail?.zScore != null ? [{ label: "Z-score", value: detail.zScore.toFixed(2), detail: detail.signal ? `hiring ${detail.signal}` : undefined,
62
+ tone: detail.signal === "surge" ? "positive" as const : detail.signal === "freeze" ? "negative" as const : undefined }] : []),
63
+ ] : [],
64
+ sections: {
65
+ table: { columns: COMPANY_COLUMNS, rows: companies.map(companyRow), empty: "Hiring observations are not available yet." },
66
+ chart: { columns: HISTORY_COLUMNS, rows: [...history].reverse().map((row): AttentionRow => ({ id: row.week, date: row.week, values: { date: row.week, open: row.openCount, added: row.added, removed: row.removed, net: row.netChange, change: row.changePct, remote: row.remoteShare == null ? null : row.remoteShare * 100, coverage: row.completeness }, details: [{ label: "Observed", value: row.observedAt }, { label: "Confidence", value: percent(row.confidence) }, { label: "Capture", value: row.kind }, { label: "Evergreen roles", value: count(row.evergreenCount) }] })), empty: data.preview ? "Full hiring history requires Pro." : "History is accumulating from the first capture." },
67
+ mix: { columns: [column("label", mixKey === "signals" ? "Signal" : "Group", 28, false, true), column("count", mixKey === "signals" ? "Value" : "Roles", 12, true), column("share", mixKey === "signals" ? "Confidence %" : "Share %", mixKey === "signals" ? 14 : 30, true), ...(mixKey === "signals" ? [column("date", "Observed", 12)] : [])], rows, empty: "No classified observations for this group yet." },
68
+ peers: { columns: COMPANY_COLUMNS, rows: (detail?.peers ?? []).map(companyRow), empty: "No comparable mapped peers yet." },
69
+ evidence: { columns: [column("title", "Role", 32, false, true), column("location", "Location", 24), column("function", "Function", 18), column("seniority", "Seniority", 13), column("date", "Observed", 12), column("confidence", "Confidence %", 13, true)], rows: (detail?.evidence ?? []).map((row): AttentionRow => ({ id: `${row.snapshotId}:${row.id}`, url: row.url ?? row.sourceUrl, values: { title: row.title, location: row.location, function: roleFamily(row.jobFunction), seniority: row.seniority ? humanLabel(row.seniority) : null, date: row.observedAt.slice(0, 10), confidence: row.confidence * 100 }, details: [{ label: "Posting title", value: row.title }, { label: "Location", value: row.location ?? "--" }, { label: "Country", value: row.country ?? "Unknown" }, { label: "Function", value: row.jobFunction ?? "Unclassified" }, { label: "Seniority", value: row.seniority ?? "Unclassified" }, { label: "Remote", value: row.remote === null ? "Unknown" : row.remote ? "Yes" : "No" }, { label: "Observed", value: row.observedAt }, { label: "Revision", value: String(row.revision) }, { label: "Confidence", value: percent(row.confidence) }, { label: "Reporting ticker", value: row.sourceTicker }] })), empty: "No primary posting evidence is available yet." },
70
+ },
71
+ chart: { label: "Open roles", unit: "roles", points: history.map((point) => ({ date: point.week, value: point.openCount })) },
72
+ };
73
+ if (detail) {
74
+ model.sections.table = { ...model.sections.evidence, columns: model.sections.evidence.columns.filter((entry) => entry.id !== "confidence"), empty: "No current role evidence has been captured yet." };
75
+ }
76
+ return model;
77
+ }
78
+
79
+ export function attentionSeries(model: Pick<AttentionModel, "chart">, color: string) {
80
+ // One or two captures are observations, not an established trend.
81
+ if (model.chart.points.filter((point) => point.value !== null).length < 3) return [];
82
+ const cadence = model.chart.unit === "roles" ? 7 * 86_400_000 : 86_400_000;
83
+ const points = [...model.chart.points].sort((a, b) => a.date.localeCompare(b.date)).flatMap((point, i, all) => {
84
+ const time = Date.parse(point.date), previous = i ? Date.parse(all[i - 1]!.date) : null;
85
+ const gap = previous !== null && time - previous > cadence * 1.5;
86
+ return [...(gap ? [scalarPoint(new Date(previous + cadence), null)] : []), scalarPoint(new Date(time), point.value)];
87
+ });
88
+ return [{ ...staticSeries(points, { id: "company-attention", label: model.chart.label, color, calendarSpaced: true }), unit: model.chart.unit, unitGroup: model.chart.unit }];
89
+ }
90
+
91
+ export function sortAttentionRows(rows: AttentionRow[], columnId: string, direction: "asc" | "desc") {
92
+ return [...rows].sort((a, b) => {
93
+ const left = a.values[columnId], right = b.values[columnId];
94
+ if (left == null) return right == null ? a.id.localeCompare(b.id) : 1;
95
+ if (right == null) return -1;
96
+ const result = typeof left === "number" && typeof right === "number" ? left - right : String(left).localeCompare(String(right));
97
+ return (direction === "asc" ? result : -result) || a.id.localeCompare(b.id);
98
+ });
99
+ }
100
+
101
+ const APP_COLUMNS = [column("name", "App", 27, false, true), column("symbol", "Ticker", 12), column("country", "Country", 9), column("chart", "Chart", 10), column("rank", "Rank", 7, true), column("change", "7D places", 12, true), column("rating", "Rating / 5", 12, true), column("ratings", "Ratings", 12, true), column("drift", "7D rating", 12, true), column("growth", "7D ratings", 12, true)];
102
+ const APP_COMPANIES = [column("symbol", "Ticker", 12), column("name", "Company", 24, false, true), column("score", "Score / 100", 13, true), column("velocity", "Places / day", 14, true), column("apps", "Apps", 8, true), column("countries", "Countries", 11, true), column("drift", "7D rating", 12, true), column("growth", "7D ratings", 12, true)];
103
+ const appCompanyRow = (row: AppCompany): AttentionRow => ({ id: row.symbol, symbol: row.symbol, values: { symbol: row.symbol, name: row.name, score: row.attentionScore, velocity: row.rankVelocity7d, apps: row.appCount, countries: row.countryCount, drift: row.ratingChange7d, growth: row.ratingCountGrowth7d } });
104
+ export function appsModel(data: AppAttentionPayload, mix = "countries"): AttentionModel {
105
+ const evidence = data.evidence.map((row): AttentionRow => ({ id: `${row.revisionId}:${row.appId}`, url: row.sourceUrl, values: { name: row.name, country: row.country, chart: row.chart, rank: row.rank, observed: row.observedAt.slice(0, 10), confidence: row.confidence * 100 }, details: [{ label: "App", value: row.name }, { label: "App ID", value: row.appId }, { label: "Country / chart", value: `${row.country} / ${row.chart}` }, { label: "Rank", value: count(row.rank) }, { label: "Rating", value: row.rating?.toFixed(2) ?? "--" }, { label: "Ratings", value: count(row.ratingCount) }, { label: "Written reviews", value: count(row.reviewCount) }, { label: "Observed", value: row.observedAt }, { label: "Published", value: row.sourceUpdatedAt ?? "Unavailable" }, { label: "Revision", value: row.revisionId }, { label: "Supersedes", value: row.supersedesRevisionId ?? "Initial capture" }, { label: "Mapping confidence", value: percent(row.confidence) }, ...(row.mappingSourceUrl ? [{ label: "Ownership evidence", value: row.mappingSourceUrl }] : []), ...(row.ratingSourceUrl ? [{ label: "Rating evidence", value: row.ratingSourceUrl }] : [])] }));
106
+ return {
107
+ asOf: data.summary.lastObservedAt,
108
+ preview: data.access === "preview",
109
+ locked: Object.values(data.locked).reduce((a, b) => a + b, 0),
110
+ notices: [...data.coverage.limitations, ...(data.apps.some((row) => row.stale) ? ["Some app observations are stale."] : [])],
111
+ // Seven-day figures appear once a week of captures can give them.
112
+ figures: [
113
+ { label: "Attention score", value: data.summary.attentionScore?.toFixed(1) ?? "--", detail: "/ 100" },
114
+ ...(data.summary.rankVelocity7d != null ? [{ label: "Rank velocity", value: signed(data.summary.rankVelocity7d, 2), detail: "places / day" }]
115
+ : [{ label: "Apps", value: count(data.summary.appCount), detail: `${count(data.summary.countryCount)} countries` }]),
116
+ ...(data.summary.ratingChange7d != null ? [{ label: "Rating drift", value: signed(data.summary.ratingChange7d, 2), detail: "7D / 5" }] : []),
117
+ ...(data.summary.ratingCountGrowth7d != null ? [{ label: "Ratings added", value: signed(data.summary.ratingCountGrowth7d), detail: "7D" }] : []),
118
+ ],
119
+ sections: {
120
+ table: { columns: APP_COLUMNS, rows: data.apps.map((row): AttentionRow => ({ id: `${row.sourceId}:${row.appId}`, app: { store: row.store, appId: row.appId, name: row.name, country: row.country, chart: row.chart }, symbol: row.symbol ?? undefined, url: row.appUrl, values: { name: row.name, symbol: row.symbol, country: row.country, chart: row.chart, rank: row.rank, change: row.rankChange7d, rating: row.rating, ratings: row.ratingCount, drift: row.ratingChange7d, growth: row.ratingCountGrowth7d }, details: [{ label: "App", value: row.name }, { label: "Developer", value: row.developer }, { label: "Parent ticker", value: row.symbol ?? "Unmapped" }, { label: "Genres", value: row.genres.join(", ") || "--" }, { label: "Observed", value: row.observedAt }, { label: "Ratings observed", value: row.ratingObservedAt ?? "Unavailable" }, { label: "Mapping confidence", value: percent(row.confidence) }, { label: "Mapping revision", value: row.mappingRevisionId ?? "Unmapped" }, { label: "Capture revision", value: row.revisionId }] })), empty: "No app observations match these filters yet." },
121
+ chart: { columns: [column("date", "Date", 12), column("score", "Score / 100", 14, true), column("change", "Change", 10, true), column("apps", "Apps", 9, true), column("countries", "Countries", 12, true), column("rating", "Rating / 5", 12, true), column("ratings", "Ratings", 13, true)], rows: data.history.map((point, i, all): AttentionRow => ({ id: point.date, date: point.date, values: { date: point.date, score: point.attentionScore, change: point.attentionScore != null && all[i - 1]?.attentionScore != null ? point.attentionScore - all[i - 1]!.attentionScore! : null, apps: point.appCount, countries: point.countryCount, rating: point.rating, ratings: point.ratingCount } })).reverse(), empty: data.access === "preview" ? "Full app history requires Pro." : "History is accumulating from the first capture." },
122
+ mix: mix === "spreads" ? { columns: [column("name", "App", 28, false, true), column("chart", "Chart", 10), column("best", "Best country", 15), column("bestRank", "Best rank", 11, true), column("worst", "Worst country", 15), column("worstRank", "Worst rank", 12, true), column("spread", "Rank spread", 13, true)], rows: data.spreads.map((row): AttentionRow => ({ id: `${row.appId}:${row.chart}:${row.category}`, values: { name: row.name, chart: row.chart, best: row.bestCountry, bestRank: row.bestRank, worst: row.worstCountry, worstRank: row.worstRank, spread: row.spread } })), empty: "Comparable country observations are accumulating." }
123
+ : { columns: [column("country", "Country", 16, false, true), column("apps", "Apps", 9, true), column("ranked", "Ranked apps", 13, true), column("score", "Score / 100", 14, true), column("velocity", "Places / day", 15, true), column("date", "Observed", 12)], rows: data.countries.map((row): AttentionRow => ({ id: row.country, values: { country: row.country, apps: row.appCount, ranked: row.rankedApps, score: row.attentionScore, velocity: row.rankVelocity7d, date: row.observedAt.slice(0, 10) } })), empty: "Country observations are not available yet." },
124
+ peers: { columns: APP_COMPANIES, rows: (data.symbol ? data.peers : data.companies).map(appCompanyRow), empty: "No comparable mapped companies yet." },
125
+ evidence: { columns: [column("name", "App", 27, false, true), column("country", "Country", 10), column("chart", "Chart", 10), column("rank", "Rank", 8, true), column("observed", "Observed", 12), column("confidence", "Confidence %", 14, true)], rows: evidence, empty: "No primary chart evidence is available yet." },
126
+ },
127
+ chart: { label: "Attention score", unit: "/100", points: data.history.map((point) => ({ date: point.date, value: point.attentionScore })) },
128
+ };
129
+ }
@@ -0,0 +1,183 @@
1
+ import { useCallback, useMemo, useRef } from "react";
2
+ import { isAccessDenied } from "../../../api-client/errors";
3
+ import { usePlanAccess } from "../../../api-client/plan-access";
4
+ import type { AppAttentionFilter, AppAttentionPayload, AppChart } from "../../../api-client/app-attention";
5
+ import type { HiringBoard, HiringPayload } from "../../../api-client/hiring";
6
+ import { ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, PaneFooterScope, PaneStatusBody, QueryBar, useChartTableSelection, usePagedRows, useTableLoadMore, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type PaneHint } from "../../../components";
7
+ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
8
+ import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
9
+ import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
10
+ import { useThemeColors } from "../../../theme/theme-context";
11
+ import type { PaneProps } from "../../../types/plugin";
12
+ import { Box, ScrollBox, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
13
+ import { SignInWall } from "../cloud/auth-actions";
14
+ import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
15
+ import { Blurred, LockedOverlay, UpgradeLabel } from "../shared/locked-rows";
16
+ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
17
+ import { humanLabel, listingCell, missingCell, shareCell, signedColor, toneColor, withoutQuietColumns, type StateTone } from "../shared/research-cells";
18
+ import { AppRankView } from "./app-rank";
19
+ import { useAttentionEvidence } from "./evidence";
20
+ import { cachedAttention, fetchAttention, loadAttention, validateAttention, type AttentionPayload } from "./client";
21
+ import { appsModel, attentionSeries, count, hiringModel, sortAttentionRows, type AttentionKind, type AttentionRow, type AttentionTab } from "./model";
22
+
23
+ type Size = Pick<PaneProps, "width" | "height" | "focused">;
24
+ const rowId = (row: AttentionRow) => row.id;
25
+ const rowDate = (row: AttentionRow) => row.date ? new Date(row.date) : null;
26
+ /** Decimals per column: percentages and scores to one place, ratings and z-scores to two, counts whole. */
27
+ const DECIMALS: Record<string, number> = { score: 1, remote: 1, share: 1, confidence: 0, velocity: 2, rating: 2, drift: 2, z: 2, change: 1 };
28
+ const SIGNED_COLUMNS = new Set(["net", "change", "velocity", "drift", "growth"]);
29
+ const SIGNAL_TONES: Record<string, StateTone> = { Surge: "positive", Freeze: "negative", Stale: "warning", Collecting: "muted", Uncovered: "muted", Ok: "text" };
30
+ /** Columns left out while every row reads the same default: a first capture has no weekly change yet. */
31
+ const QUIET_COLUMNS: Record<string, readonly string[]> = { net: [""], z: [""], change: [""], velocity: [""], drift: [""], growth: [""], added: [""], removed: [""], coverage: ["complete"], spread: [""] };
32
+ const LABEL_COLUMNS = new Set(["chart"]);
33
+ function Evidence({ row, width, height }: Size & { row: AttentionRow }) {
34
+ const colors = useThemeColors();
35
+ const desktop = !!useUiCapabilities().nativePaneChrome;
36
+ return <ScrollBox width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
37
+ {(row.details ?? Object.entries(row.values).map(([label, value]) => ({ label, value: String(value ?? "--") }))).map((entry) => <KeyValueRow key={entry.label} label={entry.label} value={entry.value} labelWidth={22} />)}
38
+ {row.url ? <Box paddingY={1}><Text fg={colors.textMuted}>{row.url}</Text></Box> : null}
39
+ </ScrollBox>;
40
+ }
41
+
42
+ export function HiringPane(props: Size) {
43
+ const { symbol } = usePaneTickerIdentity();
44
+ return <AttentionView key={symbol ?? "board"} {...props} kind="hiring" symbol={symbol ?? undefined} />;
45
+ }
46
+ export function AppsPane(props: Size) {
47
+ const { symbol } = usePaneTickerIdentity();
48
+ const [appId] = usePaneSettingValue<string>("appId", "");
49
+ const [store] = usePaneSettingValue<string>("store", "app-store");
50
+ const [country] = usePaneSettingValue<string>("country", "");
51
+ const [chart] = usePaneSettingValue<string>("chart", "");
52
+ const [name] = usePaneSettingValue<string>("appRankName", appId);
53
+ const session = useResearchCloudSession();
54
+ const access = usePlanAccess();
55
+ const focus = useMemo(() => ({ appId, name, country: (country || (store === "google-play" ? "GLOBAL" : "US")).toUpperCase(), chart: (chart || (store === "google-play" ? "unranked" : "free")) as AppChart, store: store === "google-play" ? "google-play" as const : "app-store" as const }), [appId, name, country, chart, store]);
56
+ if (appId) return <AppRankView {...props} focus={focus} accessKey={`${session.requestKey}:${access.hasProAccess ? "full" : "preview"}`} />;
57
+ return <AttentionView key={symbol ?? "board"} {...props} kind="apps" symbol={symbol ?? undefined} />;
58
+ }
59
+
60
+ function AttentionView({ kind, symbol, width, height, focused }: Size & { kind: AttentionKind; symbol?: string }) {
61
+ const colors = useThemeColors();
62
+ const desktop = !!useUiCapabilities().nativePaneChrome;
63
+ const host = useRendererHost();
64
+ const { createPaneFromTemplate } = usePluginAppActions();
65
+ const session = useResearchCloudSession();
66
+ const access = usePlanAccess();
67
+ const accessKey = `${session.requestKey}:${access.hasProAccess ? "full" : "preview"}`;
68
+ const upgrade = useCloudUpgradeAction(kind === "hiring" ? "hire" : "apps");
69
+ const [openingTab] = usePaneSettingValue<string>("tab", "table");
70
+ const [savedTab, setTab] = usePluginPaneState<string>(`${kind}:${symbol ?? "board"}:tab`, openingTab);
71
+ const tab: AttentionTab = ["chart", "mix", "peers", "evidence"].includes(savedTab) && (kind === "apps" || symbol) ? savedTab as AttentionTab : "table";
72
+ const [openingCountry] = usePaneSettingValue<string>("country", "");
73
+ const [country, setCountry] = usePluginPaneState<string>(`${kind}:country`, openingCountry);
74
+ const [openingChart] = usePaneSettingValue<string>("chart", "");
75
+ const [chart, setChart] = usePluginPaneState<string>(`${kind}:chart`, openingChart);
76
+ const [openingDays] = usePaneSettingValue<string>("days", "90");
77
+ const [days, setDays] = usePluginPaneState<string>(`${kind}:days`, String(openingDays));
78
+ const [mix, setMix] = usePluginPaneState<string>(`${kind}:mix`, kind === "hiring" ? "functions" : "countries");
79
+ const filters = useMemo<AppAttentionFilter>(() => kind === "apps" ? { ...(country ? { country } : {}), ...(chart ? { chart: chart as AppChart } : {}), days: Number(days) || 90 } : {}, [kind, country, chart, days]);
80
+ const [snapshotSetting] = usePaneSettingValue<AttentionPayload | null>("attentionSnapshot", null);
81
+ const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch { return null; } }, [kind, snapshotSetting]);
82
+ const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadAttention(kind, symbol, accessKey, filters, force), [kind, symbol, accessKey, filters, snapshot]);
83
+ const resource = useAsyncResource(loader, { initialData: () => cachedAttention(kind, symbol, accessKey, filters), clearOnError: isAccessDenied });
84
+ const data = resource.data?.payload;
85
+ const model = useMemo(() => data ? kind === "hiring" ? hiringModel(data as HiringPayload | HiringBoard, mix) : appsModel(data as AppAttentionPayload, mix) : null, [data, kind, mix]);
86
+ const [selectedId, setSelected] = usePluginPaneState<string | null>(`${kind}:${symbol ?? "board"}:${tab}:selected`, null);
87
+ const [openCompany, setOpenCompany] = usePluginPaneState<string | null>(`${kind}:${symbol ?? "board"}:company`, null);
88
+ const [openAppId, setOpenApp] = usePluginPaneState<string | null>(`${kind}:${symbol ?? "board"}:app`, null);
89
+ const [openEvidence, setOpenEvidence] = usePluginPaneState<string | null>(`${kind}:${symbol ?? "board"}:evidence`, null);
90
+ const [sort, setSort] = usePluginPaneState<{ column: string; direction: "asc" | "desc" }>(`${kind}:${symbol ?? "board"}:${tab}:sort`, { column: tab === "chart" ? "date" : tab === "mix" ? "count" : kind === "hiring" ? symbol ? "title" : "open" : "rank", direction: tab === "chart" || tab === "mix" || (kind === "hiring" && !symbol) ? "desc" : "asc" });
91
+ const section = model?.sections[tab];
92
+ const paginated = kind === "apps" && ["table", "evidence", "peers"].includes(tab) || kind === "hiring" && !symbol;
93
+ const pageLoader = useCallback(async ({ offset, signal }: { offset: number; signal: AbortSignal; force: boolean }) => {
94
+ const payload = offset === 0 && data ? data : await fetchAttention(kind, symbol, { ...filters, offset, limit: 100 }, undefined, signal);
95
+ const projected = kind === "hiring" ? hiringModel(payload as HiringPayload | HiringBoard, mix) : appsModel(payload as AppAttentionPayload, mix);
96
+ const nextOffset = "companies" in payload && kind === "hiring" && "total" in payload
97
+ ? !payload.preview && offset + payload.companies.length < payload.total ? offset + payload.companies.length : null
98
+ : (payload as AppAttentionPayload).page?.nextOffset ?? null;
99
+ return { rows: projected.sections[tab].rows, hasMore: !snapshot && nextOffset !== null, nextOffset };
100
+ }, [data, kind, symbol, filters, mix, tab, snapshot]);
101
+ const paged = usePagedRows(data && paginated ? pageLoader : null, { getId: rowId });
102
+ const sectionRows = paginated && paged.status !== "idle" ? paged.rows : section?.rows ?? [];
103
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
104
+ const scrollMore = useTableLoadMore(scrollRef, paged.hasMore && !openCompany && !openEvidence, paged.loadMore);
105
+ const lockedCount = !data || !model?.preview ? 0 : typeof data.locked === "number" ? data.locked
106
+ : kind === "hiring" ? (data as HiringPayload).locked[tab === "table" || tab === "evidence" ? "evidence" : tab === "chart" ? "history" : tab === "peers" ? "peers" : mix === "signals" ? "signals" : "mix"]
107
+ : (data as AppAttentionPayload).locked[tab === "table" ? "apps" : tab === "chart" ? "history" : tab === "mix" ? mix === "spreads" ? "spreads" : "countries" : tab === "peers" ? symbol ? "peers" : "companies" : "evidence"];
108
+ const lockedRows = Math.min(3, lockedCount);
109
+ const rows = useMemo(() => [...sortAttentionRows(sectionRows, sort.column, sort.direction), ...Array.from({ length: lockedRows }, (_, index): AttentionRow => ({ id: `locked:${index}`, values: {} }))], [sectionRows, sort, lockedRows]);
110
+ useAttentionEvidence(kind, data ?? null, tab, rows.filter((row) => !row.id.startsWith("locked:")).map(rowId));
111
+ const dataRows = useMemo(() => rows.filter((row) => !row.id.startsWith("locked:")), [rows]);
112
+ // A company's own apps all carry its ticker; the column only earns its room on the board.
113
+ const columns = useMemo(() => !section ? [] : withoutQuietColumns(section.columns.filter((column) => !(column.id === "symbol" && kind === "apps" && symbol && tab !== "peers")),
114
+ dataRows, (row, id) => row.values[id] ?? "", QUIET_COLUMNS), [section, dataRows, kind, symbol, tab]);
115
+ const maxShare = Math.max(0, ...dataRows.map((row) => typeof row.values.share === "number" ? row.values.share : 0));
116
+ const selected = rows.find((row) => row.id === selectedId) ?? rows[0] ?? null;
117
+ const evidence = rows.find((row) => row.id === openEvidence) ?? null;
118
+ const app = rows.find((row) => row.id === openAppId)?.app ?? null;
119
+ const detailOpen = !!openCompany || !!evidence || !!app;
120
+ const tabs = [{ value: "table", label: "Table" }, ...(kind === "apps" || symbol ? [{ value: "chart", label: "Chart" }, { value: "mix", label: kind === "hiring" ? "Mix" : "Markets" }, { value: "peers", label: kind === "apps" && !symbol ? "Companies" : "Peers" }, { value: "evidence", label: "Evidence" }] : [])];
121
+ const { strip, rows: tabRows } = usePaneTabs(model && !openCompany && !app ? { tabs, activeValue: tab, onSelect: (value) => { setTab(value); setOpenEvidence(null); }, focused, dense: true } : null);
122
+ const bodyHeight = Math.max(3, height - tabRows);
123
+ const series = useMemo(() => model ? attentionSeries(model, colors.textBright) : [], [model, colors.textBright]);
124
+ const chartSelection = useChartTableSelection({ rows, getId: rowId, getDate: rowDate, selectedId: selected?.id ?? null, onSelect: setSelected, focused: focused && !detailOpen, enabled: tab === "chart" });
125
+ useAutoRefresh(resource.updatedAt, resource.load);
126
+ usePaneRefreshKey(() => { void resource.reload(); paged.reload(); }, { focused: focused && !detailOpen });
127
+ const selectedSymbol = selected?.symbol ?? symbol;
128
+ const source = evidence?.url ?? selected?.url;
129
+ const hints: PaneHint[] = [
130
+ ...(source ? [{ id: "source", key: "o", label: "pen source", onPress: () => void host.openExternal(source) }] : []),
131
+ ...(selected && !evidence && selected.details ? [{ id: "evidence", key: "e", label: "vidence", onPress: () => setOpenEvidence(selected.id) }] : []),
132
+ ...(selected?.app && selectedSymbol && !symbol ? [{ id: "company", key: "c", label: "ompany", onPress: () => setOpenCompany(selectedSymbol) }] : []),
133
+ ...(selectedSymbol ? [
134
+ { id: "description", key: "d", label: "es", onPress: () => createPaneFromTemplate("new-ticker-detail-pane", { symbol: selectedSymbol }) },
135
+ { id: "financials", key: "f", label: "a", onPress: () => createPaneFromTemplate("financial-analysis-pane", { symbol: selectedSymbol }) },
136
+ { id: "chart", key: "g", label: "raph", onPress: () => createPaneFromTemplate("chart-composer-pane", { arg: selectedSymbol }) },
137
+ { id: "related", key: "a", label: kind === "hiring" ? "pps" : " hiring", title: kind === "hiring" ? "App attention" : "Hiring momentum", onPress: () => createPaneFromTemplate(kind === "hiring" ? "apps-pane" : "hiring-pane", { symbol: selectedSymbol }) },
138
+ ] : []),
139
+ ...(model?.preview ? [{ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: upgrade }] : []),
140
+ ];
141
+ usePaneStatusFooter({ registrationId: `${kind}:status`, enabled: !openCompany && !app, loading: resource.loading || paged.loadingMore, error: data ? resource.error ?? paged.moreError?.message ?? paged.error?.message : null, stale: resource.data?.stale,
142
+ info: [...(model?.asOf ? [{ id: "observed", parts: [{ text: `as of ${model.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []), ...(model?.preview ? [{ id: "preview", parts: [{ text: "Pro preview", tone: "warning" as const }] }] : [])], hints });
143
+ usePaneNoticeFooter({ registrationId: `${kind}:notices`, enabled: !openCompany && !app, focused, notices: [...(model?.notices ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
144
+ if (!data && isCloudSessionRequired(resource.error)) return <SignInWall placement={kind === "hiring" ? "hiring-momentum-signin" : "app-attention-signin"} action={kind === "hiring" ? "view hiring momentum" : "view app attention"} needsVerification={session.needsVerification} />;
145
+ const countryOptions = data && kind === "apps" ? (data as AppAttentionPayload).coverage.configuredCountries : [];
146
+ const query = kind === "hiring" && tab !== "mix" ? undefined : <QueryBar width={width} filters={kind === "apps" ? [
147
+ { id: "country", label: "Country", value: country, defaultValue: "", options: [{ value: "", label: "All countries" }, ...countryOptions.map((value) => ({ value, label: value }))], onChange: setCountry },
148
+ { id: "chart", label: "Chart", value: chart, defaultValue: "", options: [{ value: "", label: "All charts" }, { value: "free", label: "Free" }, { value: "paid", label: "Paid" }, { value: "unranked", label: "Unranked" }], onChange: setChart },
149
+ ...(tab === "chart" ? [{ id: "days", label: "Window", value: days, options: ["30", "90", "365", "730"].map((value) => ({ value, label: `${value}D` })), onChange: setDays }] : []),
150
+ ...(tab === "mix" ? [{ id: "mix", label: "View", value: mix, options: [{ value: "countries", label: "Countries" }, { value: "spreads", label: "Rank spreads" }], onChange: setMix }] : []),
151
+ ] : tab === "mix" ? [{ id: "mix", label: "Group", value: mix, options: [{ value: "functions", label: "Role family" }, { value: "seniority", label: "Seniority" }, { value: "countries", label: "Countries" }, { value: "locations", label: "Locations" }, { value: "signals", label: "Signals" }], onChange: setMix }] : []} />;
152
+ return <Box width={width} height={height} flexDirection="column">
153
+ {strip}
154
+ <PaneStatusBody loading={!data && resource.loading} error={!data ? resource.error : null} subject={kind === "hiring" ? "hiring momentum" : "app attention"}>
155
+ {model && section ? <PageStackView focused={focused} detailOpen={detailOpen} onBack={() => { setOpenCompany(null); setOpenEvidence(null); setOpenApp(null); }} detailTitle={openCompany ?? app?.name ?? (evidence ? String(evidence.values.title ?? evidence.values.name ?? evidence.id) : undefined)}
156
+ detailContent={openCompany ? <PaneFooterScope active><AttentionView key={openCompany} kind={kind} symbol={openCompany} width={width} height={bodyHeight - 1} focused={focused} /></PaneFooterScope> : app ? <AppRankView focus={app} accessKey={accessKey} width={width} height={bodyHeight - 1} focused={focused} /> : evidence ? <Evidence row={evidence} width={width} height={bodyHeight - 1} focused={focused} /> : null}
157
+ rootContent={<DataTableView columns={columns} items={rows} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen} scrollRef={scrollRef} onBodyScrollActivity={scrollMore}
158
+ rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={rows.length} tableColumns={columns} query={query} figures={model.figures}
159
+ chart={tab === "chart" ? { series, ...chartSelection, formatValue: (value) => kind === "hiring" ? count(value) : value.toFixed(1), empty: series.length ? undefined : model.preview ? "Full history requires Pro." : "Collecting history", remoteKind: `${kind}-history` } : null} />}
160
+ selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelected }} getItemKey={rowId}
161
+ sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((old) => ({ column, direction: old.column === column && old.direction === "desc" ? "asc" : "desc" }))}
162
+ onActivate={(row) => row.id.startsWith("locked:") ? void upgrade() : row.app ? setOpenApp(row.id) : row.symbol && row.symbol !== symbol && (tab === "peers" || !symbol) ? setOpenCompany(row.symbol) : row.details ? setOpenEvidence(row.id) : row.url ? void host.openExternal(row.url) : undefined}
163
+ renderCell={(row, column, _index, state) => {
164
+ if (row.id.startsWith("locked:")) {
165
+ if (!desktop && column.id === columns[0]?.id && row.id === "locked:0") return { text: "Upgrade for all observations", content: <UpgradeLabel text="Upgrade for all observations" onPress={upgrade} role="attention-upgrade" /> };
166
+ return desktop ? { text: "", content: <Blurred><Text fg={colors.textDim}>Hidden</Text></Blurred> } : { text: "░░░░", color: colors.textDim };
167
+ }
168
+ const value = row.values[column.id];
169
+ if (value == null || value === "") return missingCell(colors);
170
+ if (column.id === "symbol" && typeof value === "string") return listingCell(value, colors, state.selected);
171
+ if (column.id === "signal" && typeof value === "string") return { text: value, color: toneColor(SIGNAL_TONES[value] ?? "text", colors), keepColorWhenSelected: value === "Surge" || value === "Freeze" };
172
+ const numeric = typeof value === "number";
173
+ const decimals = DECIMALS[column.id] ?? 0;
174
+ const text = numeric ? `${SIGNED_COLUMNS.has(column.id) && value > 0 ? "+" : ""}${value.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}` : LABEL_COLUMNS.has(column.id) ? humanLabel(value) : column.id === "title" ? value.trim() : value;
175
+ if (column.id === "share" && numeric && tab === "mix") return { ...shareCell(text, maxShare ? value / maxShare : null, column.width, colors, state.selected, desktop), value };
176
+ return { text, value, color: numeric && SIGNED_COLUMNS.has(column.id) ? signedColor(value, colors)
177
+ : column.id === "name" || column.id === "title" ? colors.textBright : column.id === "date" || column.id === "observed" || column.id === "confidence" ? colors.textDim : colors.text };
178
+ }} selectedTextOverridesCellColor showHorizontalScrollbar resetScrollKey={`${kind}:${symbol}:${tab}:${mix}:${country}:${chart}`}
179
+ emptyStateTitle={section.empty} bodyAfter={lockedRows && desktop ? <LockedOverlay rows={lockedRows} text="Upgrade for all observations" onPress={upgrade} role="attention-upgrade" /> : undefined} />}
180
+ /> : <EmptyState title="Observations are not available yet." />}
181
+ </PaneStatusBody>
182
+ </Box>;
183
+ }
@@ -0,0 +1,92 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import type { GuidancePayload, KpiEvidence, KpiGuidance, KpiObservation, KpiPeriod, KpiQueryOptions, KpisPayload } from "../../../api-client/company-kpis";
3
+ import { createPluginCache } from "../../../data/plugin-cache";
4
+ import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
5
+
6
+ export type CompanyDataset = KpisPayload | GuidancePayload;
7
+ export type CompanyMode = "kpis" | "guidance";
8
+ export const KPI_UNAVAILABLE = "Company disclosures are not available yet.";
9
+ export const companyKpisCache = createPluginCache<CompanyDataset>({ kind: "company-kpis", source: "gloom-cloud", schemaVersion: 1,
10
+ policy: { staleMs: 60 * 60_000, expireMs: 30 * 86_400_000 } });
11
+ const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
12
+ const text = (value: unknown): value is string => typeof value === "string";
13
+ const nullableText = (value: unknown) => value === null || text(value);
14
+ const date = (value: unknown) => text(value) && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
15
+ const instant = (value: unknown) => text(value) && Number.isFinite(Date.parse(value));
16
+ const count = (value: unknown) => Number.isInteger(value) && Number(value) >= 0;
17
+ const confidence = (value: unknown) => finite(value) && value >= 0 && value <= 1;
18
+ const nullableNumber = (value: unknown) => value === null || finite(value);
19
+ const units = ["currency", "currency_per_share", "currency_per_unit", "percent", "basis_points", "count", "ratio", "volume"];
20
+ const bases = ["reported", "adjusted", "constant_currency", "organic"];
21
+ const sourceKinds = ["press_release", "filing", "transcript", "opendart", "ir", "esef", "edinet"];
22
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
23
+ function validPeriod(period: KpiPeriod): boolean {
24
+ return !!period && (period.end === null || date(period.end)) && (period.start === null || date(period.start) && (period.end === null || period.start <= period.end))
25
+ && ["quarter", "half", "year", "ytd", "instant", "other"].includes(period.kind) && text(period.label)
26
+ && (period.fiscalYear === null || Number.isInteger(period.fiscalYear))
27
+ && (period.fiscalQuarter === null || [1, 2, 3, 4].includes(period.fiscalQuarter));
28
+ }
29
+ function validEvidence(evidence: KpiEvidence): boolean {
30
+ let safeUrl = false;
31
+ try { safeUrl = ["https:", "http:"].includes(new URL(evidence.url).protocol); } catch { /* Invalid evidence cannot be opened. */ }
32
+ return !!evidence && text(evidence.id) && text(evidence.documentId) && text(evidence.sourceId)
33
+ && sourceKinds.includes(evidence.sourceKind) && safeUrl && text(evidence.title) && instant(evidence.publishedAt)
34
+ && text(evidence.country) && text(evidence.language) && text(evidence.quote) && evidence.quote.trim().length > 0
35
+ && count(evidence.quoteOffset) && Number.isInteger(evidence.quoteSourceLength) && evidence.quoteSourceLength > 0
36
+ && ["exact", "whitespace", "nfkc_whitespace"].includes(evidence.quoteMatchMode) && confidence(evidence.confidence);
37
+ }
38
+ function validRow(row: KpiObservation | KpiGuidance): boolean {
39
+ return !!row && text(row.id) && text(row.symbol) && text(row.metricId) && !!row.metric && row.metric.id === row.metricId
40
+ && text(row.metric.name) && text(row.metric.definition) && units.includes(row.unit) && bases.includes(row.basis)
41
+ && nullableText(row.currency) && (!row.unit.startsWith("currency") || !!row.currency)
42
+ && record(row.dimensions) && Object.values(row.dimensions).every(text) && validPeriod(row.period)
43
+ && instant(row.asOf) && text(row.seriesKey) && confidence(row.confidence) && count(row.revision)
44
+ && nullableText(row.supersedesId) && typeof row.current === "boolean" && Array.isArray(row.evidence)
45
+ && row.evidence.length > 0 && row.evidence.every(validEvidence);
46
+ }
47
+ function validObservation(row: KpiObservation) { return validRow(row) && finite(row.value) && typeof row.conflict === "boolean" && (row.contested === undefined || typeof row.contested === "boolean")
48
+ && (row.valueQualifier === undefined || ["exact", "approximately", "at_least", "at_most", "greater_than", "less_than"].includes(row.valueQualifier)); }
49
+ function validGuidance(row: KpiGuidance) {
50
+ return validRow(row) && date(row.issuedDate) && [row.low, row.high, row.point, row.midpointChangePct].every(nullableNumber)
51
+ && (row.low === null || row.high === null || row.low <= row.high)
52
+ && ["exact", "approximately", "at_least", "at_most", "greater_than", "less_than", "qualitative", "withdrawn"].includes(row.hedge)
53
+ && ["active", "withdrawn"].includes(row.status) && text(row.rangeText) && nullableText(row.previousId)
54
+ && (row.conditions === undefined || nullableText(row.conditions))
55
+ && ["raised", "cut", "reiterated", "mixed", "initiated", "withdrawn", "not_comparable"].includes(row.direction)
56
+ && (row.actual === null || text(row.actual.observationId) && finite(row.actual.value)
57
+ && ["above", "below", "within"].includes(row.actual.outcome) && ["beat", "miss", "in_line", "neutral"].includes(row.actual.favorable)
58
+ && finite(row.actual.difference) && nullableNumber(row.actual.differencePct));
59
+ }
60
+ export function validateCompanyData<T extends CompanyDataset>(data: T): T {
61
+ const coverage = data?.coverage;
62
+ const common = !!data && text(data.symbol) && ["available", "unavailable"].includes(data.status)
63
+ && (data.asOf === null || instant(data.asOf)) && ["full", "preview"].includes(data.access)
64
+ && count(data.lockedRows) && count(data.totalRows) && typeof data.truncated === "boolean"
65
+ && (data.previewRows === null || count(data.previewRows)) && text(data.methodology)
66
+ && !!coverage && [coverage.documents, coverage.observations, coverage.guidance, coverage.conflicts].every(count)
67
+ && Array.isArray(coverage.languages) && coverage.languages.every(text) && Array.isArray(coverage.sourceKinds)
68
+ && coverage.sourceKinds.every((kind) => sourceKinds.includes(kind)) && Array.isArray(data.dictionary);
69
+ const valid = common && ("series" in data
70
+ ? Array.isArray(data.series) && data.series.every((series) => !!series && text(series.key) && validObservation(series.latest) && series.latest.seriesKey === series.key
71
+ && Array.isArray(series.observations) && series.observations.every((row) => validObservation(row) && row.seriesKey === series.key))
72
+ && Array.isArray(data.revisions) && data.revisions.every(validObservation)
73
+ : Array.isArray(data.guidance) && data.guidance.every(validGuidance) && Array.isArray(data.history) && data.history.every(validGuidance));
74
+ if (!valid) throw new Error("The server returned unreadable company disclosures");
75
+ return data;
76
+ }
77
+ type CompanyClient = Pick<typeof apiClient, "getCloudCompanyKpis" | "getCloudCompanyGuidance">;
78
+ export async function fetchCompanyData(mode: CompanyMode, symbol: string, options: KpiQueryOptions = {}, client: CompanyClient = apiClient) {
79
+ try {
80
+ const data = validateCompanyData(await (mode === "kpis" ? client.getCloudCompanyKpis(symbol, options) : client.getCloudCompanyGuidance(symbol, options)));
81
+ if (("series" in data) !== (mode === "kpis") || data.symbol.toUpperCase() !== symbol.trim().toUpperCase()) throw new Error("Company disclosures do not match the requested company");
82
+ return data;
83
+ }
84
+ catch (error) { throw unavailableOnServer(error, KPI_UNAVAILABLE, [404, 503]); }
85
+ }
86
+ export const companyQuery = (options: Record<string, unknown>): KpiQueryOptions => Object.fromEntries(["metric", "basis", "from", "to", "asOf"]
87
+ .flatMap((key) => typeof options[key] === "string" && options[key] !== "all" ? [[key, options[key]]] : [])) as KpiQueryOptions;
88
+ // Partition paid history by both identity and entitlement, including screenshot snapshots.
89
+ const cacheKey = (mode: CompanyMode, symbol: string, access: string) => `${mode}:${symbol.toUpperCase()}:${access}`;
90
+ export const cachedCompanyData = (mode: CompanyMode, symbol: string, access: string) => cachedCloudResource(companyKpisCache, cacheKey(mode, symbol, access), validateCompanyData);
91
+ export const loadCompanyData = (mode: CompanyMode, symbol: string, access: string, force = false) => loadCloudResource(companyKpisCache,
92
+ cacheKey(mode, symbol, access), () => fetchCompanyData(mode, symbol), { force, validate: validateCompanyData });
@@ -0,0 +1,32 @@
1
+ import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
+ import { companyQuery, fetchCompanyData, type CompanyMode } from "./client";
3
+
4
+ export function companyHeadless(mode: CompanyMode): HeadlessPaneDefinition<"bundle"> {
5
+ return {
6
+ shape: "bundle",
7
+ argument: { kind: "ticker", description: "Listed company ticker, including exchange-qualified global symbols.", placeholder: "ticker" },
8
+ discovery: { aliases: [mode === "kpis" ? "KPIS" : "GUIDE"], dataRequirements: ["Gloom Cloud company disclosures"],
9
+ limitations: ["Pro dataset with a fixed latest preview on Free", "Coverage follows available company disclosures; missing data is not zero", "Compare matching definition, period, currency, basis and scope", "Fiscal labels without explicit calendar dates are retained without inferred dates"] },
10
+ options: [
11
+ { key: "tab", type: "enum", settingKey: "tab", defaultValue: "table", values: [{ value: "table" }, { value: "chart" }, ...(mode === "guidance" ? [{ value: "history" }] : []), { value: "evidence" }], description: "The pane section to render." },
12
+ { key: "metric", type: "string", settingKey: "metric", description: "Canonical metric ID, such as arr or subscribers." },
13
+ { key: "basis", type: "enum", values: [{ value: "reported" }, { value: "adjusted" }, { value: "constant_currency" }, { value: "organic" }], description: "Accounting basis filter (Pro)." },
14
+ { key: "from", type: "string", description: "First calendar period end, YYYY-MM-DD (Pro)." },
15
+ { key: "to", type: "string", description: "Last calendar period end, YYYY-MM-DD (Pro)." },
16
+ { key: "asOf", type: "string", description: "Point-in-time publication cutoff, YYYY-MM-DD (Pro)." },
17
+ ],
18
+ describe: (args) => `${mode === "kpis" ? "Company KPIs" : "Company guidance"} | ${args.symbols[0]}`,
19
+ async load(args, ctx) {
20
+ const options = companyQuery(args.options);
21
+ const data = await fetchCompanyData(mode, args.symbols[0]!, options, ctx.apiClient);
22
+ return { complete: !data.truncated, errors: data.truncated ? [data.access === "preview" ? "Full disclosures and history require Gloom Pro" : "Response limit reached; narrow the metric or date range"] : [],
23
+ sections: "series" in data ? [
24
+ { title: "Latest KPIs", rows: data.series.map((series) => ({ ...series.latest })) },
25
+ { title: "KPI history", rows: data.series.flatMap((series) => series.observations.map((row) => ({ ...row }))) },
26
+ { title: "Revisions and conflicts", rows: data.revisions.map((row) => ({ ...row })) },
27
+ ] : [{ title: "Current guidance", rows: data.guidance.map((row) => ({ ...row })) }, { title: "Guidance history", rows: data.history.map((row) => ({ ...row })) }],
28
+ metadata: { symbol: data.symbol, asOf: data.asOf, access: data.access, coverage: data.coverage, dictionary: data.dictionary,
29
+ lockedRows: data.lockedRows, totalRows: data.totalRows, methodology: data.methodology } };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,26 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
3
+ import { companyKpisCache } from "./client";
4
+ import { companyHeadless } from "./headless";
5
+ import { CompanyGuidancePane, CompanyKpisPane } from "./pane";
6
+
7
+ const kpisHeadless = companyHeadless("kpis");
8
+ const guidanceHeadless = companyHeadless("guidance");
9
+ export const companyKpisModule: PluginModule = {
10
+ setup(ctx) { companyKpisCache.attach(ctx.persistence); },
11
+ dispose() { companyKpisCache.reset(); },
12
+ panes: [
13
+ { id: "company-kpis", name: "Company KPIs", icon: "K", component: CompanyKpisPane, defaultPosition: "right", defaultMode: "floating",
14
+ defaultFloatingSize: { width: 130, height: 30 }, tickerFollower: true, tableExport: true, headless: kpisHeadless },
15
+ { id: "company-guidance", name: "Company Guidance", icon: "G", component: CompanyGuidancePane, defaultPosition: "right", defaultMode: "floating",
16
+ defaultFloatingSize: { width: 130, height: 30 }, tickerFollower: true, tableExport: true, headless: guidanceHeadless },
17
+ ],
18
+ paneTemplates: [
19
+ { ...createTickerSurfacePaneTemplate({ id: "company-kpis-pane", paneId: "company-kpis", label: "Company KPIs", shortcut: "KPIS",
20
+ description: "Pro company operating KPIs, comparable history, revisions and cited evidence. Free preview.",
21
+ keywords: ["kpis", "operating metrics", "arr", "subscribers", "backlog", "occupancy"] }), headless: kpisHeadless },
22
+ { ...createTickerSurfacePaneTemplate({ id: "company-guidance-pane", paneId: "company-guidance", label: "Company Guidance", shortcut: "GUIDE",
23
+ description: "Pro management guidance ranges, raises and cuts, actual versus guide and cited evidence. Free preview.",
24
+ keywords: ["guidance", "outlook", "raise", "cut", "beat", "miss"] }), headless: guidanceHeadless },
25
+ ],
26
+ };