@gloom-sh/terminal 0.18.5 → 0.19.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 (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -55,7 +55,9 @@ import {
55
55
  import { colors } from "../../../theme/colors";
56
56
  import { isUsListingExchange, parsePublicTickerKey, publicTickerKey } from "../../../utils/exchanges";
57
57
  import { getTimeSeriesField, isMarketFieldId } from "../../../time-series/field-catalog";
58
- import { isDatasetChart } from "../../../time-series/spec";
58
+ import { isDatasetChart, isOhlcSeriesStyle } from "../../../time-series/spec";
59
+ import { useThemeColors } from "../../../theme/theme-context";
60
+ import { priceComparisonSeriesIds } from "../../../time-series/price-comparison";
59
61
  import { seriesMessageName, studyMessageName } from "../../../time-series/series-name";
60
62
  import { CHART_COMPOSER_PANE_ID } from "../../../types/config";
61
63
  import { useRemoteUiNode } from "../../../remote/semantic-tree";
@@ -117,6 +119,8 @@ import {
117
119
  CHART_BAR_SIZE_PRESETS,
118
120
  CHART_RANGES as RANGES,
119
121
  CHART_RESOLUTIONS,
122
+ CHART_CANDLE_COLOURS_SETTING_KEY,
123
+ chartCandleColours,
120
124
  chartStudyOptionsFor,
121
125
  chartStudyPeriodTitle,
122
126
  chartStudySettingLabel,
@@ -342,6 +346,14 @@ function ChartComposerSurface({
342
346
  // plausible time is dropped without pinning. This handle only sets the
343
347
  // pin; the chart it opens on is what shows and keeps it.
344
348
  const [pinnedMoment, setPinnedMoment] = usePaneSettingValue<unknown>(CHART_PINNED_MOMENT_SETTING_KEY, null);
349
+ const [candleColoursSetting] = usePaneSettingValue<unknown>(CHART_CANDLE_COLOURS_SETTING_KEY, null);
350
+ const gainColour = useThemeColors().positive;
351
+ // Rising candles in the gain colour; falling ones already take the loss colour.
352
+ const candleColour = useCallback((list: ResolvedSeries[]) => (
353
+ chartCandleColours(candleColoursSetting) === "change"
354
+ ? list.map((entry) => isOhlcSeriesStyle(entry.style) ? { ...entry, color: gainColour } : entry)
355
+ : list
356
+ ), [candleColoursSetting, gainColour]);
345
357
  const momentLink = useTabCursorLink(paneId ? `${paneId}:moment` : null, { viewer: false });
346
358
  useEffect(() => {
347
359
  if (pinnedMoment == null || !momentLink?.pin) return;
@@ -475,9 +487,9 @@ function ChartComposerSurface({
475
487
  });
476
488
  const resolvedIds = new Set(resolved.map((entry) => entry.id));
477
489
  const missing = spec.series.filter((entry) => !resolvedIds.has(entry.id));
478
- if (missing.length === 0) return resolved;
490
+ if (missing.length === 0) return candleColour(resolved);
479
491
  const bySpecOrder = new Map(spec.series.map((entry, index) => [entry.id, index] as const));
480
- return [
492
+ return candleColour([
481
493
  ...resolved,
482
494
  ...missing.map((entry): ResolvedSeries => ({
483
495
  id: entry.id,
@@ -495,15 +507,15 @@ function ChartComposerSurface({
495
507
  hidden: true,
496
508
  points: [],
497
509
  })),
498
- ].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
499
- }, [listings.names, nameSeries, resolution.legendSeries, spec.series]);
510
+ ].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER)));
511
+ }, [candleColour, listings.names, nameSeries, resolution.legendSeries, spec.series]);
500
512
  const plottedSeries = useMemo(
501
- () => projectVisibleChartSeries(
513
+ () => candleColour(projectVisibleChartSeries(
502
514
  spec,
503
515
  resolution.bufferedSeries ?? resolution.series,
504
516
  resolution.legendSeries,
505
- ),
506
- [resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
517
+ )),
518
+ [candleColour, resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
507
519
  );
508
520
  const { notify } = usePluginAppActions();
509
521
  const { runtime } = usePluginRenderContext();
@@ -1190,6 +1202,22 @@ function ChartComposerSurface({
1190
1202
  }]
1191
1203
  : [];
1192
1204
 
1205
+ // Returns of two or more prices compare as price returns; the toggle reinvests their cash distributions.
1206
+ const totalReturnOn = spec.viewport.totalReturn === true;
1207
+ const totalReturnFilters = totalReturnOn || priceComparisonSeriesIds(spec) !== null
1208
+ ? [{
1209
+ id: "total-return",
1210
+ kind: "toggle" as const,
1211
+ label: "Total return",
1212
+ short: "Total ret",
1213
+ value: totalReturnOn,
1214
+ onChange: (value: boolean) => {
1215
+ const { totalReturn: _previous, ...viewport } = specRef.current.viewport;
1216
+ updateSpec({ ...specRef.current, viewport: value ? { ...viewport, totalReturn: true } : viewport });
1217
+ },
1218
+ }]
1219
+ : [];
1220
+
1193
1221
  // A company that reports no quarterly figures says so, and where its annual ones are.
1194
1222
  const unreported = resolution.legendSeries?.find((entry) => entry.unreportedPeriod && entry.points.length === 0);
1195
1223
  const emptyMessage = spec.series.length === 0
@@ -1216,6 +1244,7 @@ function ChartComposerSurface({
1216
1244
  onChange: (value: string) => setRange(value as TimeRange) },
1217
1245
  ...genericFilters,
1218
1246
  ...extendedHoursFilters,
1247
+ ...totalReturnFilters,
1219
1248
  ]}
1220
1249
  view={resolutionOptions.length > 1 ? {
1221
1250
  value: spec.viewport.resolution,
@@ -129,6 +129,20 @@ export const CHART_SETTING_KEYS = {
129
129
  realizedVolEstimator: "chartRealizedVolEstimator",
130
130
  } as const;
131
131
 
132
+ /**
133
+ * Whether rising candles take their series' colour or the theme's gain colour.
134
+ * Kept beside the spec in the pane's settings, so a build without it opens the
135
+ * chart unchanged; it is not one of CHART_SETTING_KEYS, which a spec edit
136
+ * clears as derived from the spec.
137
+ */
138
+ export const CHART_CANDLE_COLOURS_SETTING_KEY = "chartCandleColours";
139
+
140
+ export type ChartCandleColours = "series" | "change";
141
+
142
+ export function chartCandleColours(value: unknown): ChartCandleColours {
143
+ return value === "change" ? "change" : "series";
144
+ }
145
+
132
146
  function fallbackSpec(symbol: string | null | undefined): ChartSpec {
133
147
  return symbol ? buildPriceChartPreset(symbol) : buildEmptyChartPreset();
134
148
  }
@@ -417,6 +431,7 @@ export function buildChartComposerPaneSettingsDef(
417
431
  [CHART_SETTING_KEYS.range]: spec.viewport.range,
418
432
  [CHART_SETTING_KEYS.resolution]: spec.viewport.resolution,
419
433
  [CHART_SETTING_KEYS.mode]: inlineStyleTarget?.style ?? "",
434
+ [CHART_CANDLE_COLOURS_SETTING_KEY]: chartCandleColours(settings[CHART_CANDLE_COLOURS_SETTING_KEY]),
420
435
  ...(realizedVol ? {
421
436
  [CHART_SETTING_KEYS.realizedVolWindow]: String(realizedVol.parameters.window ?? 30),
422
437
  [CHART_SETTING_KEYS.realizedVolEstimator]: realizedVol.parameters.estimator ?? "close-to-close",
@@ -494,6 +509,18 @@ export function buildChartComposerPaneSettingsDef(
494
509
  options: modes.map((mode) => ({ value: mode, label: mode.toUpperCase() })),
495
510
  }]
496
511
  : []),
512
+ ...(spec.series.some((series) => isOhlcSeriesStyle(series.style))
513
+ ? [{
514
+ key: CHART_CANDLE_COLOURS_SETTING_KEY,
515
+ label: "Candle Colours",
516
+ description: "Draw rising candles in their series' colour, or in the theme's gain colour against its loss colour.",
517
+ type: "select" as const,
518
+ options: [
519
+ { value: "series", label: "Series colour" },
520
+ { value: "change", label: "Gains & losses" },
521
+ ],
522
+ }]
523
+ : []),
497
524
  ],
498
525
  applyValue: applyChartComposerPaneSetting,
499
526
  };
@@ -55,6 +55,7 @@ export function filterCommandSuggestions(
55
55
  const byCode: ChatFunction[] = [];
56
56
  const byName: ChatFunction[] = [];
57
57
  for (const fn of new Set(functions.values())) {
58
+ if (fn.unlisted) continue;
58
59
  if (fn.code.startsWith(upper) || fn.aliases.includes(upper)) byCode.push(fn);
59
60
  else if (fn.name.toLowerCase().split(/[\s/&()-]+/).some((word) => word.startsWith(lower))) byName.push(fn);
60
61
  }
@@ -36,6 +36,8 @@ export interface ChatFunction {
36
36
  arg: ChatFunctionArg;
37
37
  /** For a choice, the values it takes. */
38
38
  choices?: () => readonly string[];
39
+ /** Written as a chip when someone types it, but never suggested (`listed: false`). */
40
+ unlisted?: true;
39
41
  }
40
42
 
41
43
  /** A function named in a message, and the text the command bar runs for it. */
@@ -106,7 +108,11 @@ export function chatFunctionsFrom(source: ChatFunctionSource, core: readonly Com
106
108
  // A switched-off plugin's code opens nothing until it is turned on, and
107
109
  // leaves the code to a function that does.
108
110
  if (source.isTemplateOff?.(template.id)) continue;
109
- add(upperCodes(paneTemplateShortcutPrefixes(template)), { name: getPaneTemplateDisplayLabel(template), ...templateArg(template) });
111
+ add(upperCodes(paneTemplateShortcutPrefixes(template)), {
112
+ name: getPaneTemplateDisplayLabel(template),
113
+ ...templateArg(template),
114
+ ...(template.listed === false ? { unlisted: true as const } : {}),
115
+ });
110
116
  }
111
117
  return byCode;
112
118
  }
@@ -267,12 +267,3 @@ export function trialLayout(width: number): TrialLayout {
267
267
  const statusAt = fixed.findIndex((column) => column.id === "status") + 1;
268
268
  return { compact, showVenue: wide, columns: [...fixed.slice(0, statusAt), title, ...fixed.slice(statusAt)] };
269
269
  }
270
-
271
- /**
272
- * A search typed as a ticker (`TRIAL VKTX`): one to five letters, with a
273
- * share class. The registry knows sponsors by name, so a symbol no study
274
- * mentions is looked up as its company.
275
- */
276
- export function looksLikeTicker(term: string): boolean {
277
- return /^[A-Za-z]{1,5}(?:[.-][A-Za-z])?$/.test(term.trim());
278
- }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  import {
3
3
  DataTableStackView,
4
4
  PaneLinkMenu,
@@ -41,7 +41,6 @@ import {
41
41
  SPONSOR_FILTER_OPTIONS,
42
42
  STATUS_FILTER_OPTIONS,
43
43
  keepsToneWhenSelected,
44
- looksLikeTicker,
45
44
  normalizeTrialFilters,
46
45
  sponsorClassLabel,
47
46
  trialFiltersNarrow,
@@ -57,7 +56,7 @@ import {
57
56
  type TrialFilters,
58
57
  } from "./model";
59
58
  import { useSponsorTickers, type SponsorTicker } from "./sponsor-tickers";
60
- import { tickerSponsorName } from "./ticker-sponsor";
59
+ import { looksLikeTicker, useTickerCompanySearch } from "../shared/ticker-company";
61
60
  import { CLINICAL_TRIALS_PANE_ID } from "./types";
62
61
 
63
62
  const SEARCH_DEBOUNCE_MS = 250;
@@ -187,7 +186,6 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
187
186
  }, [client, filtersKey, term]);
188
187
  const studies = usePagedRows(loadPage, { getId: (trial) => trial.nctId, keepPreviousRows: true });
189
188
  const trials = studies.rows;
190
- const error = studies.error?.message ?? null;
191
189
  const loadMore = useTableLoadMore(scrollRef, studies.hasMore && !openItemId, studies.loadMore);
192
190
  // Scrolling asks for the next page, so a page that does not fill a tall
193
191
  // pane asks for it once it is laid out.
@@ -230,28 +228,23 @@ export function TrialsPane({ width, height, focused }: PaneProps) {
230
228
  setOpenItemId(null);
231
229
  }, [filters, setOpenItemId, setSavedFilters, setSelectedId]);
232
230
 
233
- const reload = useCallback(() => {
234
- studies.reload();
235
- sponsorTickers.retry();
236
- }, [sponsorTickers, studies]);
237
231
  // `TRIAL VKTX`: no study names a symbol, so one that finds nothing is
238
232
  // searched as its company's name, quoted so the registry matches the whole
239
233
  // name as a sponsor files it, and the search box then shows it. Each term
240
234
  // is looked up once.
241
- const [looked, setLooked] = useState<ReadonlySet<string>>(() => new Set());
242
- const resolvingTicker = studies.status === "loaded" && trials.length === 0 && !studies.error
243
- && looksLikeTicker(term) && !looked.has(term.toUpperCase());
244
- useEffect(() => {
245
- if (!resolvingTicker) return;
246
- let live = true;
247
- void tickerSponsorName(dataProvider, term).catch(() => null).then((name) => {
248
- if (!live) return;
249
- setLooked((current) => new Set([...current, term.toUpperCase()]));
250
- if (name) updateQuery(`"${name}"`);
251
- });
252
- return () => { live = false; };
253
- }, [dataProvider, resolvingTicker, term, updateQuery]);
254
- const loading = (studies.loading && trials.length === 0) || resolvingTicker;
235
+ const companySearch = useTickerCompanySearch(dataProvider, term, {
236
+ eligible: studies.status === "loaded" && trials.length === 0 && !studies.error && looksLikeTicker(term),
237
+ key: term.toUpperCase(),
238
+ onCompany: (name) => updateQuery(`"${name}"`),
239
+ });
240
+ const retryCompanySearch = companySearch.retry;
241
+ const error = studies.error?.message ?? companySearch.error;
242
+ const reload = useCallback(() => {
243
+ retryCompanySearch();
244
+ studies.reload();
245
+ sponsorTickers.retry();
246
+ }, [retryCompanySearch, sponsorTickers, studies]);
247
+ const loading = (studies.loading && trials.length === 0) || companySearch.resolving;
255
248
  usePaneRefreshKey(reload, { focused, enabled: !searchFocused && !openItemId });
256
249
 
257
250
  const detailTicker = detailTrial ? tickerOf(detailTrial) : undefined;
@@ -6,6 +6,7 @@ import {
6
6
  validateOrganizationTickers,
7
7
  type OrganizationTicker,
8
8
  } from "../../../api-client/organization-tickers";
9
+ import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
9
10
 
10
11
  /**
11
12
  * A lead sponsor's ticker, only ever as Gloom Cloud resolved it. `none` is an
@@ -46,17 +47,19 @@ const sponsorKey = (name: string) => name.trim().replace(/\s+/g, " ").toLowerCas
46
47
  * never stored as having no ticker because the lookup did not happen.
47
48
  */
48
49
  export class SponsorTickerStore {
49
- private readonly answers = new Map<string, { value: SponsorTicker; expiresAt: number }>();
50
+ private readonly answers: TtlCache<SponsorTicker>;
50
51
  private readonly failures = new Map<string, string>();
51
52
  private readonly inFlight = new Set<string>();
52
53
  private readonly listeners = new Set<() => void>();
53
54
 
54
- constructor(private readonly lookup: Lookup, private readonly now: () => number = Date.now) {}
55
+ constructor(private readonly lookup: Lookup, now: () => number = Date.now) {
56
+ this.answers = createTtlCache<SponsorTicker>({ ttlMs: CACHE_TTL_MS, now });
57
+ }
55
58
 
56
59
  get(name: string): SponsorTicker {
57
60
  const key = sponsorKey(name);
58
- const answer = this.answers.get(key);
59
- if (answer && answer.expiresAt > this.now()) return answer.value;
61
+ const answer = this.answers.peek(key);
62
+ if (answer) return answer;
60
63
  const reason = this.failures.get(key);
61
64
  return reason ? { kind: "failed", reason } : { kind: "pending" };
62
65
  }
@@ -86,8 +89,7 @@ export class SponsorTickerStore {
86
89
  keys.forEach((key) => this.inFlight.add(key));
87
90
  try {
88
91
  const results = await this.lookup(batch, signal);
89
- const expiresAt = this.now() + CACHE_TTL_MS;
90
- results.forEach((result, index) => this.answers.set(keys[index]!, { value: sponsorTickerFrom(result), expiresAt }));
92
+ results.forEach((result, index) => this.answers.set(keys[index]!, sponsorTickerFrom(result)));
91
93
  } catch (error) {
92
94
  // An abandoned lookup (its pane closed) is neither an answer nor a failure.
93
95
  if (!signal?.aborted) {
@@ -142,6 +142,8 @@ function headlessCandidates(registry: PaneFunctionCatalog): ManifestCandidate[]
142
142
  const pane = registry.panes.get(template.paneId);
143
143
  if (!pane) continue;
144
144
  templatedPaneIds.add(pane.id);
145
+ // An unlisted function (`listed: false`) is no tool, and its pane none in its place.
146
+ if (template.listed === false) continue;
145
147
  const definition = definitionFor(template, pane);
146
148
  if (!definition) continue;
147
149
  const token = template.shortcut?.prefix ?? template.id;
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringBuildingFilter, HiringBuildingShift } from "../../../api-client/hiring";
@@ -60,7 +61,10 @@ export function HiringBuildingView({ symbol, width, height, focused, accessKey,
60
61
  try {
61
62
  const payload = snapshotSetting ? validateBuilding(snapshotSetting) : null;
62
63
  return payload && ("filter" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
63
- } catch { return null; }
64
+ } catch (error) {
65
+ logFailure("company-attention.building", "Validate hiring building snapshot failed")(error);
66
+ return null;
67
+ }
64
68
  }, [snapshotSetting, symbol]);
65
69
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadBuilding(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
66
70
  const resource = useAsyncResource(loader, { initialData: () => cachedBuilding(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -3,6 +3,7 @@ import type { AppAttentionFilter, AppAttentionPayload, AppRankPayload } from "..
3
3
  import type { HiringBoard, HiringBuildingBoard, HiringBuildingFilter, HiringBuildingShift, HiringCompanyBuilding, HiringCompanyMoves, HiringCompanySignals, HiringMixRow, HiringMove, HiringMovePosting, HiringMovesBoard, HiringMovesFilter, HiringMoveWeek, HiringPayload, HiringSignalGroup, HiringSignalsBoard, HiringSignalsFilter, HiringSummary, HiringWeekSignal } from "../../../api-client/hiring";
4
4
  import type { CloudJobsPostingDetail } from "../../../api-client/types";
5
5
  import { createPluginCache } from "../../../data/plugin-cache";
6
+ import { createTtlCache } from "../../../data/ttl-cache";
6
7
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
7
8
  import type { AppFocus, AttentionKind } from "./model";
8
9
 
@@ -19,7 +20,10 @@ const finite = (value: unknown) => typeof value === "number" && Number.isFinite(
19
20
  const positive = (value: unknown) => finite(value) && (value as number) >= 0;
20
21
  const fraction = (value: unknown) => positive(value) && (value as number) <= 1;
21
22
  const date = (value: unknown) => typeof value === "string" && Number.isFinite(Date.parse(value));
22
- const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch { return false; } };
23
+ const url = (value: unknown) => { try { return typeof value === "string" && ["https:", "http:"].includes(new URL(value).protocol); } catch {
24
+ // Intentional ignore: malformed URLs fail this web link field validator.
25
+ return false;
26
+ } };
23
27
  function numericTree(value: unknown): boolean {
24
28
  if (typeof value === "number") return Number.isFinite(value);
25
29
  if (Array.isArray(value)) return value.every(numericTree);
@@ -165,21 +169,16 @@ export async function fetchAppRank(focus: AppFocus, days: number, offset = 0, si
165
169
  export const loadAppRank = (focus: AppFocus, access: string, days: number, force = false) => loadCloudResource(appRankCache, `${access}:${focus.store}:${focus.appId}:${focus.country}:${focus.chart}:${days}`, () => fetchAppRank(focus, days), { force });
166
170
 
167
171
  const POSTING_TTL_MS = 5 * 60_000;
168
- const postingCache = new Map<string, { at: number; posting: Promise<CloudJobsPostingDetail> }>();
172
+ const postings = createTtlCache<CloudJobsPostingDetail>({ ttlMs: POSTING_TTL_MS });
169
173
  /**
170
174
  * The page HIRE's evidence row was read from: its title, place, date, pay,
171
175
  * hours and description. Kept a few minutes, so stepping back and forth
172
176
  * through the evidence does not read it again.
173
177
  */
174
178
  export function fetchPostingDetail(ticker: string, externalId: string, client: Pick<typeof apiClient, "getCloudJobsPosting"> = apiClient): Promise<CloudJobsPostingDetail> {
175
- const key = `${ticker.toUpperCase()}:${externalId}`;
176
- const cached = postingCache.get(key);
177
- if (cached && Date.now() - cached.at < POSTING_TTL_MS) return cached.posting;
178
- const posting = client.getCloudJobsPosting(ticker, { externalId }).then((detail) => {
179
+ return postings.load(`${ticker.toUpperCase()}:${externalId}`, async () => {
180
+ const detail = await client.getCloudJobsPosting(ticker, { externalId });
179
181
  if (!detail || typeof detail.title !== "string" || detail.externalId !== externalId) throw new Error("The server returned an unreadable posting.");
180
182
  return detail;
181
183
  });
182
- postingCache.set(key, { at: Date.now(), posting });
183
- posting.catch(() => postingCache.delete(key));
184
- return posting;
185
184
  }
@@ -45,7 +45,10 @@ export function attentionScreenshotEvidence(kind: AttentionKind): PaneScreenshot
45
45
  read(value) {
46
46
  if (!isRecord(value) || value.kind !== kind || value.version !== 1 || value.complete !== true || !isRecord(value.payload)
47
47
  || !["table", "chart", "mix", "peers", "evidence", ...(kind === "hiring" ? ["moves", "expansion", "anomalies", "building"] : [])].includes(String(value.tab)) || !Array.isArray(value.rowIds) || !value.rowIds.every((id) => typeof id === "string") || value.plottedValueCount !== value.rowIds.length) return null;
48
- try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch { return null; }
48
+ try { if (!(kind === "apps" && Array.isArray(value.payload.rankHistory))) validateAttention(kind, value.payload as unknown as AttentionPayload); return value as unknown as AttentionEvidence; } catch {
49
+ // Intentional ignore: invalid captured observations are rejected as evidence by returning null.
50
+ return null;
51
+ }
49
52
  },
50
53
  mismatches(evidence, { resolved, payload }) {
51
54
  const settings = payload.config.layout.instances.find((entry) => entry.instanceId === payload.paneId)?.settings;
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringMovesFilter } from "../../../api-client/hiring";
@@ -63,7 +64,10 @@ export function HiringMovesView({ symbol, width, height, focused, accessKey, com
63
64
  try {
64
65
  const payload = snapshotSetting ? validateMoves(snapshotSetting) : null;
65
66
  return payload && ("moves" in payload ? !symbol : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
66
- } catch { return null; }
67
+ } catch (error) {
68
+ logFailure("company-attention.moves", "Validate hiring moves snapshot failed")(error);
69
+ return null;
70
+ }
67
71
  }, [snapshotSetting, symbol]);
68
72
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadMoves(symbol, accessKey, filter, force), [snapshot, symbol, accessKey, filter]);
69
73
  const resource = useAsyncResource(loader, { initialData: () => cachedMoves(symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import { usePlanAccess } from "../../../api-client/plan-access";
@@ -57,7 +58,10 @@ function Evidence({ row, width, posting, source }: Size & { row: AttentionRow; p
57
58
  }
58
59
  /** A link's site, as a reader names it: "sec.gov". */
59
60
  function linkName(url: string): string {
60
- try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return "Source"; }
61
+ try { return new URL(url).hostname.replace(/^www\./, ""); } catch {
62
+ // Intentional ignore: a malformed link has no site to name, so it reads as Source.
63
+ return "Source";
64
+ }
61
65
  }
62
66
 
63
67
  export function HiringPane(props: Size) {
@@ -105,7 +109,10 @@ function AttentionView({ kind, symbol, width, height, focused, initialTab }: Siz
105
109
  const [mix, setMix] = usePluginPaneState<string>(`${kind}:mix`, kind === "hiring" ? "functions" : "countries");
106
110
  const filters = useMemo<AppAttentionFilter>(() => kind === "apps" ? { ...(country ? { country } : {}), ...(chart ? { chart: chart as AppChart } : {}), days: Number(days) || 90 } : {}, [kind, country, chart, days]);
107
111
  const [snapshotSetting] = usePaneSettingValue<AttentionPayload | null>("attentionSnapshot", null);
108
- const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch { return null; } }, [kind, snapshotSetting]);
112
+ const snapshot = useMemo(() => { try { return snapshotSetting ? validateAttention(kind, snapshotSetting) : null; } catch (error) {
113
+ logFailure("company-attention.pane", "Validate attention snapshot failed")(error);
114
+ return null;
115
+ } }, [kind, snapshotSetting]);
109
116
  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]);
110
117
  const resource = useAsyncResource(loader, { initialData: () => cachedAttention(kind, symbol, accessKey, filters), clearOnError: isAccessDenied });
111
118
  const data = resource.data?.payload;
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../utils/log-failure";
1
2
  import { useCallback, useMemo, useRef, type ReactNode } from "react";
2
3
  import { isAccessDenied } from "../../../api-client/errors";
3
4
  import type { HiringSignalGroup, HiringSignalsFilter, HiringWeekSignal } from "../../../api-client/hiring";
@@ -60,7 +61,10 @@ export function HiringSignalsView({ group, symbol, width, height, focused, acces
60
61
  try {
61
62
  const payload = snapshotSetting ? validateSignals(snapshotSetting) : null;
62
63
  return payload && ("group" in payload ? !symbol && payload.group === group : payload.symbol.toUpperCase() === symbol?.toUpperCase()) ? payload : null;
63
- } catch { return null; }
64
+ } catch (error) {
65
+ logFailure("company-attention.signals", "Validate hiring signals snapshot failed")(error);
66
+ return null;
67
+ }
64
68
  }, [snapshotSetting, symbol, group]);
65
69
  const loader = useCallback((force: boolean) => snapshot ? Promise.resolve({ payload: snapshot, stale: false, refreshError: null }) : loadSignals(group, symbol, accessKey, filter, force), [snapshot, group, symbol, accessKey, filter]);
66
70
  const resource = useAsyncResource(loader, { initialData: () => cachedSignals(group, symbol, accessKey, filter), clearOnError: isAccessDenied, keepPreviousData: true });
@@ -8,8 +8,9 @@ import { buildCorrelationMatrix, buildCorrelationSeries, buildGeoCorrelationSeri
8
8
  import { geoSeriesToken, loadGeoCorrelationHistory } from "./geo";
9
9
  import { buildRelationshipAnalysis, DEFAULT_RELATIONSHIP_SECOND_SYMBOL } from "./relationship/model";
10
10
  import { paneSchemas } from "./headless-schema";
11
- import { CORRELATION_RETURN_BASIS, GEO_CORRELATION_CHANGE_BASIS, loadCorrelationHistory } from "./history";
11
+ import { CORRELATION_RETURN_BASIS, GEO_CORRELATION_CHANGE_BASIS, loadCorrelationHistory, unfinishedBarFreshness } from "./history";
12
12
  import type { HeadlessPaneContext } from "../../../types/headless";
13
+ import type { PricePoint } from "../../../types/financials";
13
14
  import { newestReportTime } from "../../../utils/utc-time";
14
15
  import { DAILY_CLOSES } from "../shared/report-freshness";
15
16
 
@@ -34,9 +35,13 @@ async function loadListedHistory(ctx: HeadlessPaneContext, key: string, range: T
34
35
  }
35
36
 
36
37
  /** A ticker's daily closes, or a map series' daily values read straight from the Cloud client. */
37
- async function loadCorrelationInput(ctx: HeadlessPaneContext, key: string, range: TimeRange): Promise<CorrelationSeries> {
38
+ async function loadCorrelationInput(ctx: HeadlessPaneContext, key: string, range: TimeRange, onBars?: (bars: PricePoint[]) => void): Promise<CorrelationSeries> {
38
39
  const geo = geoSeriesToken(key);
39
- if (!geo) return buildCorrelationSeries(key, await loadHistory(ctx, key, range));
40
+ if (!geo) {
41
+ const bars = await loadHistory(ctx, key, range);
42
+ onBars?.(bars);
43
+ return buildCorrelationSeries(key, bars);
44
+ }
40
45
  const request = <T,>(path: string, init?: RequestInit) => ctx.apiClient.geo<T>(path, { ...init, signal: init?.signal ?? ctx.signal });
41
46
  return buildGeoCorrelationSeries(key, await loadGeoCorrelationHistory(request, geo, range, ctx.signal));
42
47
  }
@@ -53,7 +58,8 @@ export const correlationHeadless: HeadlessPaneDefinition<"rows"> = {
53
58
  ],
54
59
  async load({ symbols, options }, ctx) {
55
60
  const range = (options.rangePreset ?? "1Y") as TimeRange;
56
- const loaded = await loadHeadlessSymbols(symbols, ctx, (symbol) => loadCorrelationInput(ctx, symbol, range));
61
+ const bars = new Map<string, PricePoint[]>();
62
+ const loaded = await loadHeadlessSymbols(symbols, ctx, (symbol) => loadCorrelationInput(ctx, symbol, range, (points) => bars.set(symbol, points)));
57
63
  const bySymbol = new Map(loaded.entries.map(({ symbol, data }) => [symbol, data]));
58
64
  const matrix = buildCorrelationMatrix(symbols, bySymbol);
59
65
  const rows = symbols.flatMap((left, index) => symbols.slice(index + 1).map((right) => {
@@ -68,8 +74,9 @@ export const correlationHeadless: HeadlessPaneDefinition<"rows"> = {
68
74
  ? "Inconsistent OHLC history"
69
75
  : row.sampleSize < 5 ? "Insufficient shared return observations" : "Zero return variance",
70
76
  }));
77
+ const asOf = newestReportTime(symbols.map((symbol) => bySymbol.get(symbol)?.prices.at(-1)?.dateKey));
71
78
  return {
72
- freshness: { asOf: newestReportTime(symbols.map((symbol) => bySymbol.get(symbol)?.prices.at(-1)?.dateKey)) },
79
+ freshness: { asOf, ...unfinishedBarFreshness([...bars.values()], asOf?.slice(0, 10)) },
73
80
  rows, unavailableSymbols,
74
81
  errors: [
75
82
  ...loaded.errors,
@@ -132,6 +139,7 @@ export const relationshipHeadless: HeadlessPaneDefinition<"series"> = {
132
139
  })),
133
140
  { key: "returnCount", label: "Shared returns", value: analysis.stats?.sampleSize ?? analysis.returns.length },
134
141
  ],
142
+ freshness: unfinishedBarFreshness([...histories.values()], analysis.aligned.at(-1)?.dateKey) ?? {},
135
143
  unavailableSymbols: [...new Set(unavailableSymbols)],
136
144
  errors: [...loaded.errors, ...(analysis.unavailableReason ? [analysis.unavailableReason] : []),
137
145
  ...(analysis.correlationUnavailableReason ? [analysis.correlationUnavailableReason] : [])],
@@ -2,6 +2,9 @@ import type { AssetDataProvider, MarketDataRequestContext } from "../../../types
2
2
  import type { TimeRange } from "../../../time-series/range";
3
3
  import type { ManualChartResolution } from "../../../time-series/resolution";
4
4
  import { clipPriceHistoryToRange } from "../../../time-series/history-window";
5
+ import { DAY_SO_FAR_TEXT } from "../../../time-series/session-bar";
6
+ import type { PricePoint } from "../../../types/financials";
7
+ import type { HeadlessPaneFreshness } from "../../../types/headless";
5
8
 
6
9
  /** Risk views need daily closes even when a chart preset defaults to intraday or weekly bars. */
7
10
  export const CORRELATION_HISTORY_RESOLUTION: ManualChartResolution = "1d";
@@ -16,6 +19,25 @@ export async function loadCorrelationHistory(
16
19
  return clipPriceHistoryToRange(points, range);
17
20
  }
18
21
 
22
+ /**
23
+ * What an analysis of daily closes says when its newest day is a bar the source
24
+ * has not finished: a coin's UTC day or a future's session under way
25
+ * (`partial`), or a future's last trade before its settlement (`unsettled`).
26
+ * That day is no close, so the line calls the bars daily bars and says so, as
27
+ * `history` does, instead of dating a close by it. Nothing when `day` (the
28
+ * newest date the analysis uses) is a finished bar in every history.
29
+ */
30
+ export function unfinishedBarFreshness(
31
+ histories: ReadonlyArray<ReadonlyArray<Pick<PricePoint, "date" | "partial" | "unsettled">> | undefined>,
32
+ day: string | null | undefined,
33
+ ): Pick<HeadlessPaneFreshness, "basis" | "note"> | null {
34
+ if (!day) return null;
35
+ const bars = histories.flatMap((history) => history?.filter((point) => new Date(point.date).toISOString().slice(0, 10) === day) ?? []);
36
+ if (bars.some((bar) => bar.partial)) return { basis: "daily bars", note: `the newest bar is ${DAY_SO_FAR_TEXT}` };
37
+ if (bars.some((bar) => bar.unsettled)) return { basis: "daily bars", note: "the newest bar is not yet settled" };
38
+ return null;
39
+ }
40
+
19
41
  export const CORRELATION_RETURN_BASIS = "Local-price close-to-close returns between shared UTC dates; cash distributions and FX conversion are excluded, and exchange closing times may differ.";
20
42
 
21
43
  export const GEO_CORRELATION_CHANGE_BASIS = "Map series (GEO:) use the change in daily value between the same shared UTC dates, because counts such as transits can be zero.";