@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
@@ -0,0 +1,85 @@
1
+ import { ANONYMOUS_ID_STORAGE_KEY, ATTRIBUTION_STORAGE_KEY } from "../../../api-client/research-activity";
2
+ import { SYNC_CLIENT_ID_STORAGE_KEY } from "../../../sync/controller";
3
+ import { logFailure } from "../../../utils/log-failure";
4
+ import type { DesktopViewIds } from "../shared/protocol";
5
+
6
+ /**
7
+ * What the desktop view keeps only in its WebView storage: the id Cloud sync
8
+ * knows this device by, and the visitor id and first touch a gloom.sh link
9
+ * handed over. macOS and Windows keep that storage under the app's bundle
10
+ * identifier, so a build under another identifier would start without them.
11
+ * The Bun process keeps a copy in the data folder (bun/desktop/view-ids.ts),
12
+ * and a view whose storage lacks one takes it back from there.
13
+ */
14
+ const VIEW_ID_KEYS = [SYNC_CLIENT_ID_STORAGE_KEY, ANONYMOUS_ID_STORAGE_KEY, ATTRIBUTION_STORAGE_KEY] as const;
15
+
16
+ interface ViewIdStorage {
17
+ getItem(key: string): string | null;
18
+ setItem(key: string, value: string): void;
19
+ }
20
+
21
+ /** The view's WebView storage, or null where it is refused. */
22
+ export function viewIdStorage(): ViewIdStorage | null {
23
+ try {
24
+ return globalThis.localStorage ?? null;
25
+ } catch {
26
+ // Intentional ignore: without storage there is nothing to restore or keep.
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function readViewIds(storage: ViewIdStorage): DesktopViewIds {
32
+ const ids: DesktopViewIds = {};
33
+ for (const key of VIEW_ID_KEYS) {
34
+ const value = storage.getItem(key);
35
+ if (value) ids[key] = value;
36
+ }
37
+ return ids;
38
+ }
39
+
40
+ function sameIds(left: DesktopViewIds, right: DesktopViewIds): boolean {
41
+ const keys = Object.keys(left);
42
+ return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
43
+ }
44
+
45
+ /** Puts the ids kept on disk into `storage` where it has none. One it has is never replaced. */
46
+ export function restoreViewIds(storage: ViewIdStorage, kept: DesktopViewIds | undefined): void {
47
+ try {
48
+ for (const key of VIEW_ID_KEYS) {
49
+ const value = kept?.[key];
50
+ if (value && !storage.getItem(key)) storage.setItem(key, value);
51
+ }
52
+ } catch (error) {
53
+ logFailure("desktop-view-ids", "Could not restore the view's ids")(error);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * A function that sends the ids in `storage` to be kept on disk when they
59
+ * differ from the copy there (`kept`, from `init`). It reads three keys, so it
60
+ * can run whenever one of them may have been written. A save that fails is
61
+ * tried again at the next call.
62
+ */
63
+ export function createViewIdMirror(
64
+ storage: ViewIdStorage,
65
+ kept: DesktopViewIds | undefined,
66
+ save: (ids: DesktopViewIds) => Promise<unknown>,
67
+ ): () => void {
68
+ let onDisk: DesktopViewIds = { ...kept };
69
+ return () => {
70
+ let ids: DesktopViewIds;
71
+ try {
72
+ ids = readViewIds(storage);
73
+ } catch (error) {
74
+ logFailure("desktop-view-ids", "Could not read the view's ids")(error);
75
+ return;
76
+ }
77
+ if (sameIds(ids, onDisk)) return;
78
+ const previous = onDisk;
79
+ onDisk = ids;
80
+ save(ids).catch((error: unknown) => {
81
+ if (onDisk === ids) onDisk = previous;
82
+ logFailure("desktop-view-ids", "Could not send the view's ids to be kept")(error);
83
+ });
84
+ };
85
+ }
@@ -38,6 +38,9 @@ import {
38
38
  } from "../../telemetry/crash-reports-node";
39
39
  import { currentTelemetryConfig } from "../../telemetry/live-config";
40
40
  import { installUpdateFailureReporter } from "../../telemetry/update-failures";
41
+ import { installStallReporter, recordStall } from "../../telemetry/stall-reports";
42
+ import { createNodeStallReporterHost } from "../../telemetry/stall-reports-node";
43
+ import { setStallSink } from "../../utils/main-thread-stall";
41
44
  import { createNodeUpdateFailureHost } from "../../telemetry/update-failures-node";
42
45
  import { terminalInstallKind } from "../../updater";
43
46
  import { flushUsageCounts, installUsageCounter, usageCountsEnabled } from "../../telemetry/usage-counts";
@@ -124,6 +127,8 @@ export async function startOpenTuiApp({ externalPlugins, cliLaunchRequest }: Sta
124
127
  installKind: terminalInstallKind(),
125
128
  getDataDir: () => config.dataDir,
126
129
  }));
130
+ installStallReporter(createNodeStallReporterHost({ getDataDir: () => config.dataDir }));
131
+ setStallSink((event) => recordStall("terminal", event));
127
132
  installUsageCounter({
128
133
  surface: "terminal",
129
134
  os: describeNodeOs(),
@@ -63,6 +63,7 @@ export function seriesCatalogEntry(series: RegisteredSeries): SeriesCatalogEntry
63
63
  ...(definition.description ? { description: definition.description } : {}),
64
64
  ...(definition.publisher ? { publisher: definition.publisher } : {}),
65
65
  ...(definition.url ? { url: definition.url } : {}),
66
+ ...(definition.limitation ? { limitation: definition.limitation } : {}),
66
67
  ...(definition.input ? { input: definition.input } : {}),
67
68
  ...(definition.placement ? { placement: definition.placement } : {}),
68
69
  ...(definition.lines?.length ? {
@@ -133,6 +133,11 @@ interface SeriesDefinitionBase {
133
133
  publisher?: string;
134
134
  /** The publisher's page for the series. */
135
135
  url?: string;
136
+ /**
137
+ * A limit on the data the publisher sets, such as a cap on how far back its
138
+ * history goes. One sentence: the catalog lists it beside the series.
139
+ */
140
+ limitation?: string;
136
141
  /** Ask for a ticker, an option or a crypto pair before charting. */
137
142
  input?: SeriesInput;
138
143
  /**
@@ -203,6 +208,7 @@ export interface SeriesCatalogEntry {
203
208
  description?: string;
204
209
  publisher?: string;
205
210
  url?: string;
211
+ limitation?: string;
206
212
  input?: SeriesInput;
207
213
  placement?: SeriesPlacement;
208
214
  /** The lines of a series that draws several, each with the expression that charts it alone. */
@@ -136,6 +136,7 @@ export function validateSeriesDefinition(definition: SeriesDefinition, context:
136
136
  checkLabel(id, "group", definition.group, 32, context);
137
137
  checkLabel(id, "description", definition.description, 300, context);
138
138
  checkLabel(id, "publisher", definition.publisher, 60, context);
139
+ checkLabel(id, "limitation", definition.limitation, 160, context);
139
140
  if (definition.tags !== undefined) {
140
141
  if (!Array.isArray(definition.tags) || definition.tags.length > 16) fail(id, "tags must be a list of at most 16 words.");
141
142
  for (const tag of definition.tags) checkLabel(id, "tag", tag, 40, context, false);
@@ -964,6 +964,7 @@ export function createGloomberbCloudCapabilities(provider = createGloomberbCloud
964
964
  return {
965
965
  articles: cloudNewsReadArticles(read, query.ticker),
966
966
  nextCursor: read.nextCursor ?? null,
967
+ access: read.access ?? null,
967
968
  };
968
969
  },
969
970
  async fetchNews(query: NewsQuery): Promise<NewsArticle[]> {
@@ -1,7 +1,8 @@
1
1
  import { normalizeNewsFeed } from "../../news/news-model";
2
2
  import { readableNewsSource } from "../../news/source-label";
3
3
  import { collectNewsDisplayTickers } from "../../news/ticker-symbols";
4
- import type { NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
4
+ import type { NewsAccess, NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
5
+ import { CLOUD_NEWS_DELAY_HOURS } from "../../api-client/plan-rules";
5
6
  import type {
6
7
  apiClient,
7
8
  CloudNewsListResponse,
@@ -134,10 +135,44 @@ export function cloudNewsParams(query: NewsQuery): CloudNewsParams {
134
135
  };
135
136
  }
136
137
 
137
- /** A page of Gloom Cloud stories, with the text searched for when a coin pair's feed had none filed under it. */
138
- export type CloudNewsRead = CloudNewsListResponse & { searchedFor?: string };
138
+ /**
139
+ * A page of Gloom Cloud stories, with the text searched for when a coin pair's
140
+ * feed had none filed under it, and which news the service answered with.
141
+ */
142
+ export type CloudNewsRead = Omit<CloudNewsListResponse, "access"> & { searchedFor?: string; access?: NewsAccess | null };
143
+
144
+ /**
145
+ * How recently the service must have seen a story for the answer to be the
146
+ * live wire. It holds a story back from an account without real-time news
147
+ * until it last saw it {@link CLOUD_NEWS_DELAY_HOURS} hours ago; the hour
148
+ * less leaves room for this clock and the service's to disagree.
149
+ */
150
+ const LIVE_STORY_WINDOW_MS = (CLOUD_NEWS_DELAY_HOURS - 1) * 60 * 60 * 1000;
151
+
152
+ /**
153
+ * Which news a Gloom Cloud answer is, from the answer: what the service says
154
+ * when it says it; held back when the request carried no session, since the
155
+ * service holds news back for anyone it cannot name; live when it holds a
156
+ * story seen too recently to have been held back. Otherwise null: a quiet
157
+ * feed read live looks the same as a held-back one.
158
+ */
159
+ export function cloudNewsAccess(
160
+ response: Pick<CloudNewsListResponse, "items" | "access">,
161
+ sentSession: boolean,
162
+ now = Date.now(),
163
+ ): NewsAccess | null {
164
+ if (response.access === "realtime" || response.access === "delayed") return response.access;
165
+ if (!sentSession) return "delayed";
166
+ const live = response.items.some((item) => {
167
+ const seenAt = Date.parse(item.lastSeenAt);
168
+ return Number.isFinite(seenAt) && now - seenAt < LIVE_STORY_WINDOW_MS;
169
+ });
170
+ return live ? "realtime" : null;
171
+ }
139
172
 
140
- type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "searchCloudNews">;
173
+ /** A client that cannot say whether a session goes with its requests (a test double) is taken to send one. */
174
+ type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "searchCloudNews">
175
+ & Partial<Pick<typeof apiClient, "hasSessionCredential">>;
141
176
 
142
177
  /**
143
178
  * A news query's first page from Gloom Cloud. Stories are filed under company
@@ -147,13 +182,18 @@ type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "searchCloudNews"
147
182
  * no name to search keeps the empty answer.
148
183
  */
149
184
  export async function readCloudNews(client: CloudNewsClient, query: NewsQuery): Promise<CloudNewsRead> {
185
+ // Asked in the tick the request goes out in, which is when its headers take the session.
186
+ const sentSession = client.hasSessionCredential?.() ?? true;
150
187
  const response = await client.getCloudNews(cloudNewsParams(query));
151
- if (response.items.length > 0 || query.cursor || normalizeNewsFeed(query) !== "ticker") return response;
188
+ const read: CloudNewsRead = { ...response, access: cloudNewsAccess(response, sentSession) };
189
+ if (response.items.length > 0 || query.cursor || normalizeNewsFeed(query) !== "ticker") return read;
152
190
  const text = coinSearchName(query.ticker, query.exchange, query.listingName);
153
- if (!text) return response;
191
+ if (!text) return read;
154
192
  const found = await client.searchCloudNews(text, query.limit)
155
193
  .catch(logFailure("sources.gloomberb-cloud.news", "Search a coin's stories failed", null));
156
- return found && found.items.length > 0 ? { items: found.items, nextCursor: null, searchedFor: text } : response;
194
+ return found && found.items.length > 0
195
+ ? { items: found.items, nextCursor: null, searchedFor: text, access: cloudNewsAccess(found, sentSession) }
196
+ : read;
157
197
  }
158
198
 
159
199
  /** A read's stories as articles, each marked with the text a coin's fallback searched for. */
@@ -3,7 +3,7 @@ import type { AnalystResearchData, CompanyProfile, CorporateActionsData, Financi
3
3
  import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
4
4
  import { coalesceFinancialPeriodAliases, mergeFinancialStatementRows } from "../../utils/financial-statements";
5
5
  import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
6
- import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS } from "../../utils/fundamentals";
6
+ import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS, RETRACTED_FUNDAMENTALS_FIELDS } from "../../utils/fundamentals";
7
7
  import { hasValidQuoteObservationTime, isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
8
8
  import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
9
9
  import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges";
@@ -458,12 +458,21 @@ function mergeFundamentals(primary: Fundamentals | undefined, fallback: Fundamen
458
458
  const merged = mergeDefinedObject(primary, fallback);
459
459
  if (merged) {
460
460
  const unavailable = new Set(fallback?.unavailableFields ?? []);
461
- for (const field of RETRACTABLE_VALUATION_FIELDS) {
461
+ for (const field of RETRACTED_FUNDAMENTALS_FIELDS) {
462
462
  if (primary?.unavailableFields?.includes(field)) unavailable.add(field);
463
463
  else if (typeof primary?.[field] === "number" && Number.isFinite(primary[field])) unavailable.delete(field);
464
464
  }
465
465
  if (unavailable.size) merged.unavailableFields = [...unavailable];
466
466
  else delete merged.unavailableFields;
467
+ // A retracted cap is none, whatever an older observation held; its reason goes with the retraction that stands.
468
+ if (unavailable.has("marketCap")) {
469
+ delete merged.marketCap;
470
+ merged.marketCapUnavailable = primary?.unavailableFields?.includes("marketCap")
471
+ ? primary.marketCapUnavailable : fallback?.marketCapUnavailable;
472
+ if (!merged.marketCapUnavailable) delete merged.marketCapUnavailable;
473
+ } else {
474
+ delete merged.marketCapUnavailable;
475
+ }
467
476
  }
468
477
  const dividend = primary?.dividendYield != null ? primary : fallback;
469
478
  if (merged && dividend) {
@@ -557,7 +566,10 @@ export function mergeRefreshedFinancials(cached: TickerFinancials, fresh: Ticker
557
566
  const merged = mergeFinancials(cached, fresh)!;
558
567
  const statistics = redactUnavailableFundamentals(fresh?.fundamentals);
559
568
  if (!statistics) return merged;
560
- const update: Fundamentals = { unavailableFields: statistics.unavailableFields };
569
+ const update: Fundamentals = {
570
+ unavailableFields: statistics.unavailableFields,
571
+ ...statistics.marketCapUnavailable ? { marketCapUnavailable: statistics.marketCapUnavailable } : {},
572
+ };
561
573
  const currencyConflict = cached.fundamentals?.financialCurrency && statistics.financialCurrency
562
574
  && cached.fundamentals.financialCurrency !== statistics.financialCurrency;
563
575
  for (const field of RETRACTABLE_VALUATION_FIELDS) {
@@ -53,6 +53,7 @@ import { ProviderRouterSearchRoutes } from "./search";
53
53
  import { collectCapabilityRouteSources, normalizeRouteSource } from "./sources";
54
54
  import type { ProviderRouterCoreDeps } from "./route-types";
55
55
  import { withProviderConnectionHealth } from "./connection-health";
56
+ import { withFiledTrailingEps } from "../../time-series/filed-trailing-eps";
56
57
  import {
57
58
  contextFromCachedTarget,
58
59
  getBrokerCandidates,
@@ -213,7 +214,7 @@ export class AssetDataRouter implements DataProvider {
213
214
  for (const target of valid) {
214
215
  const key = target.symbol.toUpperCase();
215
216
  const financials = cached.get(key);
216
- if (financials) cached.set(key, this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target))));
217
+ if (financials) cached.set(key, withFiledTrailingEps(this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target)))));
217
218
  }
218
219
  return cached;
219
220
  }
@@ -263,8 +264,9 @@ export class AssetDataRouter implements DataProvider {
263
264
  const dated = await this.datedAnswers(quoted, options.datesChangeBasis);
264
265
  let next = 0;
265
266
  const results = answered.map((result) => {
266
- if (!result.financials?.quote) return result;
267
- const financials = this.withDatedQuote(result.financials, dated[next++]!, listingOfResult(result));
267
+ if (!result.financials) return result;
268
+ const quoted = result.financials.quote ? this.withDatedQuote(result.financials, dated[next++]!, listingOfResult(result)) : result.financials;
269
+ const financials = withFiledTrailingEps(quoted);
268
270
  return financials === result.financials ? result : { ...result, financials };
269
271
  });
270
272
  const restored = new Map(results.map((result) => {
@@ -279,10 +281,11 @@ export class AssetDataRouter implements DataProvider {
279
281
  const { context, datesChangeBasis } = splitDating(requestContext);
280
282
  exchange = this.resolvePublicExchange(ticker, exchange, context);
281
283
  const financials = await this.financialRoutes.getTickerFinancials(ticker, exchange, context);
282
- if (!financials.quote) return financials;
284
+ if (!financials.quote) return withFiledTrailingEps(financials);
283
285
  const listing = listingOf({ symbol: ticker, exchange }, context);
284
286
  const [quote] = await this.datedAnswers([{ quote: financials.quote, listing }], datesChangeBasis);
285
- return this.withDatedQuote(financials, quote!, listing);
287
+ // One trailing EPS and P/E for every view of the listing: the filed quarters', as PEB and GE read them.
288
+ return withFiledTrailingEps(this.withDatedQuote(financials, quote!, listing));
286
289
  }
287
290
 
288
291
  async getQuote(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<Quote> {
@@ -0,0 +1,50 @@
1
+ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
2
+ import { hasPendingTickerWrites, settleTickerWrites, tickerWriteRevision } from "../data/ticker-writes";
3
+ import type { TickerRecord } from "../types/ticker";
4
+
5
+ /**
6
+ * Persist cleanup before publishing it, retrying against edits acknowledged in between.
7
+ * Return the cleaned map so callers can recheck it after their config save.
8
+ */
9
+ export async function persistTickerCleanup({
10
+ tickerRepository,
11
+ getTickers,
12
+ transform,
13
+ publish,
14
+ isCurrent = () => true,
15
+ }: {
16
+ tickerRepository: AppTickerRepositoryPort;
17
+ getTickers: () => ReadonlyMap<string, TickerRecord>;
18
+ transform: (ticker: TickerRecord) => TickerRecord | null;
19
+ publish: (symbol: string, ticker: TickerRecord | null) => void;
20
+ isCurrent?: () => boolean;
21
+ }): Promise<ReadonlyMap<string, TickerRecord> | null> {
22
+ for (;;) {
23
+ do {
24
+ await settleTickerWrites(tickerRepository);
25
+ } while (hasPendingTickerWrites(tickerRepository));
26
+ if (!isCurrent()) return null;
27
+ const before = getTickers();
28
+ let wrote = false;
29
+ for (const symbol of before.keys()) {
30
+ do {
31
+ await settleTickerWrites(tickerRepository);
32
+ } while (hasPendingTickerWrites(tickerRepository));
33
+ if (!isCurrent()) return null;
34
+ const source = getTickers().get(symbol);
35
+ if (!source) continue;
36
+ const next = transform(source);
37
+ if (next === source) continue;
38
+ wrote = true;
39
+ const save = next ? tickerRepository.saveTicker(next) : tickerRepository.deleteTicker(symbol);
40
+ const revision = tickerWriteRevision(tickerRepository, symbol);
41
+ await save;
42
+ if (!isCurrent()) return null;
43
+ // An edit can start its save before publishing state, especially over RPC.
44
+ if (tickerWriteRevision(tickerRepository, symbol) !== revision || getTickers().get(symbol) !== source) continue;
45
+ publish(symbol, next);
46
+ }
47
+ // Include rows created or recreated while earlier rows were being saved.
48
+ if (!wrote && getTickers() === before) return before;
49
+ }
50
+ }
@@ -41,7 +41,8 @@ interface SyncRuntime {
41
41
  isLocalMarketData?: LocalMarketDataLookup;
42
42
  }
43
43
 
44
- const CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
44
+ /** Kept in the view's storage; the desktop app also keeps a copy on disk (electrobun/view/view-ids.ts). */
45
+ export const SYNC_CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
45
46
  const PUSH_DEBOUNCE_MS = 2500;
46
47
  /** How soon after a change contributors date it, so an edit made just before quitting still is. */
47
48
  const OBSERVE_DEBOUNCE_MS = 250;
@@ -60,10 +61,10 @@ function nowIso(): string {
60
61
 
61
62
  function resolveClientId(): string {
62
63
  const storage = globalThis.localStorage;
63
- const existing = storage?.getItem(CLIENT_ID_STORAGE_KEY);
64
+ const existing = storage?.getItem(SYNC_CLIENT_ID_STORAGE_KEY);
64
65
  if (existing) return existing;
65
66
  const id = `client_${crypto.randomUUID()}`;
66
- storage?.setItem(CLIENT_ID_STORAGE_KEY, id);
67
+ storage?.setItem(SYNC_CLIENT_ID_STORAGE_KEY, id);
67
68
  return id;
68
69
  }
69
70
 
@@ -1,4 +1,6 @@
1
1
  import { saveConfigImmediately, scheduleConfigSave } from "../state/config-save-scheduler";
2
+ import { persistTickerCleanup } from "../state/ticker-cleanup";
3
+ import { hasPendingTickerWrites } from "../data/ticker-writes";
2
4
  import { normalizeStoredBaseCurrency } from "../utils/base-currency";
3
5
  import { debugLog } from "../utils/debug-log";
4
6
  import { fnv1aHex, stableStringify } from "../utils/hash";
@@ -1184,6 +1186,23 @@ function holdingIn(positions: readonly TickerPosition[], links: readonly unknown
1184
1186
  };
1185
1187
  }
1186
1188
 
1189
+ /** Without a shared baseline, keep both devices' manual lots, matching identical copies one for one. */
1190
+ function mergeUntrackedManualPositions(local: TickerPosition[], pulled: TickerPosition[]): TickerPosition[] {
1191
+ const unmatched = new Map<string, number>();
1192
+ for (const position of local) {
1193
+ const key = JSON.stringify(sanitizePosition(position));
1194
+ unmatched.set(key, (unmatched.get(key) ?? 0) + 1);
1195
+ }
1196
+ const merged = [...local];
1197
+ for (const position of pulled) {
1198
+ const key = JSON.stringify(sanitizePosition(position));
1199
+ const count = unmatched.get(key) ?? 0;
1200
+ if (count > 0) unmatched.set(key, count - 1);
1201
+ else merged.push(position);
1202
+ }
1203
+ return merged;
1204
+ }
1205
+
1187
1206
  interface PulledTickersMerge {
1188
1207
  lastSyncedTickers: Map<string, Record<string, unknown>> | null;
1189
1208
  /** This device's live broker portfolios, each with the side holding its latest broker sync. */
@@ -1274,8 +1293,25 @@ function mergePulledTicker(
1274
1293
  holdingIn(baselinePositions, baselineLinks, portfolioId),
1275
1294
  );
1276
1295
  const [sidePositions, sideLinks] = takePulled ? [pulledPositions, pulledLinks] : [localPositions, localLinks];
1277
- positions.push(...sidePositions.filter((position) => position.portfolio === portfolioId));
1278
- if (sideLinks.includes(portfolioId)) portfolios.push(portfolioId);
1296
+ if (brokerSide && !merge.localOnly?.portfolioIds.has(portfolioId)) {
1297
+ // A broker's timestamp governs imported holdings, never hand-entered
1298
+ // lots. Those use the same edit comparison as a manual portfolio.
1299
+ const manualIn = (entries: readonly TickerPosition[]) => entries.filter((position) => (
1300
+ position.portfolio === portfolioId && position.broker === "manual"
1301
+ ));
1302
+ const localManual = manualIn(localPositions);
1303
+ const manualEdited = comparable && !sameAsStored(
1304
+ localManual.map(sanitizePosition), manualIn(baselinePositions).map(sanitizePosition),
1305
+ );
1306
+ const pulledManual = manualIn(pulledPositions);
1307
+ const manual = !comparable ? mergeUntrackedManualPositions(localManual, pulledManual)
1308
+ : manualEdited ? localManual : pulledManual;
1309
+ positions.push(...sidePositions.filter((position) => position.portfolio === portfolioId && position.broker !== "manual"), ...manual);
1310
+ if (sideLinks.includes(portfolioId) || manual.length > 0) portfolios.push(portfolioId);
1311
+ } else {
1312
+ positions.push(...sidePositions.filter((position) => position.portfolio === portfolioId));
1313
+ if (sideLinks.includes(portfolioId)) portfolios.push(portfolioId);
1314
+ }
1279
1315
  }
1280
1316
 
1281
1317
  const record: TickerRecord = {
@@ -1297,8 +1333,9 @@ function mergePulledTicker(
1297
1333
  function withoutUnpulledHoldings(ticker: TickerRecord, merge: PulledTickersMerge): TickerRecord | null {
1298
1334
  const sold = (portfolioId: string) => merge.brokerPortfolios.get(portfolioId) === "pulled";
1299
1335
  const { metadata } = ticker;
1300
- const positions = metadata.positions.filter((position) => !sold(position.portfolio));
1301
- const portfolios = metadata.portfolios.filter((portfolioId) => !sold(portfolioId));
1336
+ const positions = metadata.positions.filter((position) => position.broker === "manual" || !sold(position.portfolio));
1337
+ const held = new Set(positions.map((position) => position.portfolio));
1338
+ const portfolios = metadata.portfolios.filter((portfolioId) => held.has(portfolioId) || !sold(portfolioId));
1302
1339
  const kept = positions.length === metadata.positions.length && portfolios.length === metadata.portfolios.length
1303
1340
  ? ticker
1304
1341
  : { ...ticker, metadata: { ...metadata, positions, portfolios } };
@@ -1322,12 +1359,57 @@ export const coreConfigSyncContributor: SyncContributor = {
1322
1359
  },
1323
1360
  apply: async (payload, { snapshot, baselinePayload, baselineState, state, getState, isCurrent, dispatch, tickerRepository, memo }) => {
1324
1361
  const pushedAt = snapshot?.contributors[CORE_CONFIG_CONTRIBUTOR_ID]?.updatedAt;
1325
- const nextConfig = mergeConfigPayload(state.config, payload, baselineState.config, baselinePayload, {
1362
+ const merge = (config: AppConfig) => mergeConfigPayload(config, payload, baselineState.config, baselinePayload, {
1326
1363
  memo: memo?.get(),
1327
1364
  pushedAt,
1328
- }, localOnlyScope(state.config));
1365
+ }, localOnlyScope(config));
1366
+ let nextConfig = merge(state.config);
1329
1367
  if (!nextConfig) return;
1330
- const configChanged = !valuesEqual(nextConfig, state.config);
1368
+ const removed = removedBrokerHoldings(state.config, nextConfig);
1369
+ if (removed) {
1370
+ // Keep the profile until row and config writes succeed, so a failed
1371
+ // cleanup remains visible to the next attempt.
1372
+ const cleanTickers = () => persistTickerCleanup({
1373
+ tickerRepository,
1374
+ getTickers: () => getState().tickers,
1375
+ transform: (ticker) => withoutRemovedBrokerHoldings(ticker, removed),
1376
+ publish: (symbol, ticker) => {
1377
+ const tickers = new Map(getState().tickers);
1378
+ if (ticker) tickers.set(symbol, ticker);
1379
+ else tickers.delete(symbol);
1380
+ dispatch({ type: "SET_TICKERS", tickers });
1381
+ },
1382
+ isCurrent,
1383
+ });
1384
+ let savedRemoval = false;
1385
+ for (;;) {
1386
+ const cleaned = await cleanTickers().catch(async (error: unknown) => {
1387
+ // Restore the profile if a late row fails after config was saved,
1388
+ // so retry can also recover after restarting the app.
1389
+ if (savedRemoval) await saveConfigImmediately(getState().config);
1390
+ throw error;
1391
+ });
1392
+ if (!cleaned || !isCurrent()) return;
1393
+ if (hasPendingTickerWrites(tickerRepository) || getState().tickers !== cleaned) continue;
1394
+ const source = getState().config;
1395
+ const merged = merge(source);
1396
+ if (!merged) return;
1397
+ // Preserve unrelated edits while retaining the removal already applied
1398
+ // to rows, including pulls from builds that merged whole profile lists.
1399
+ nextConfig = {
1400
+ ...merged,
1401
+ brokerInstances: merged.brokerInstances.filter((instance) => !removed.instanceIds.has(instance.id)),
1402
+ portfolios: merged.portfolios.filter((portfolio) => !removed.portfolioIds.has(portfolio.id)),
1403
+ };
1404
+ await saveConfigImmediately(nextConfig);
1405
+ savedRemoval = true;
1406
+ if (!isCurrent()) return;
1407
+ if (getState().config !== source || getState().tickers !== cleaned
1408
+ || hasPendingTickerWrites(tickerRepository)) continue;
1409
+ break;
1410
+ }
1411
+ }
1412
+ const configChanged = !valuesEqual(nextConfig, getState().config);
1331
1413
  if (configChanged) {
1332
1414
  // Adopting another device's workspace rearranges the screen under the
1333
1415
  // user, so it is worth a line in the log when a report says "my panes
@@ -1339,32 +1421,9 @@ export const coreConfigSyncContributor: SyncContributor = {
1339
1421
  });
1340
1422
  }
1341
1423
  dispatch({ type: "SET_CONFIG", config: nextConfig });
1342
- scheduleConfigSave(nextConfig);
1424
+ if (!removed) scheduleConfigSave(nextConfig);
1343
1425
  }
1344
1426
  if (memo && isRecord(payload)) rememberCloudLayout(payload, pushedAt, getState().config, memo, nextConfig.layouts !== state.config.layouts);
1345
- if (!configChanged) return;
1346
-
1347
- // A profile removed on another device takes its positions with it here
1348
- // too; kept, this device would push them back and recreate the tickers
1349
- // that removal deleted.
1350
- const removed = removedBrokerHoldings(state.config, nextConfig);
1351
- if (!removed) return;
1352
- const nextTickers = new Map(getState().tickers);
1353
- let changed = false;
1354
- for (const ticker of getState().tickers.values()) {
1355
- if (!isCurrent()) return;
1356
- const next = withoutRemovedBrokerHoldings(ticker, removed);
1357
- if (next === ticker) continue;
1358
- changed = true;
1359
- if (next) {
1360
- await tickerRepository.saveTicker(next);
1361
- nextTickers.set(next.metadata.ticker, next);
1362
- } else {
1363
- await tickerRepository.deleteTicker(ticker.metadata.ticker);
1364
- nextTickers.delete(ticker.metadata.ticker);
1365
- }
1366
- }
1367
- if (changed && isCurrent()) dispatch({ type: "SET_TICKERS", tickers: nextTickers });
1368
1427
  },
1369
1428
  };
1370
1429
 
@@ -0,0 +1,38 @@
1
+ import type { StallEvent } from "../utils/main-thread-stall";
2
+
3
+ /**
4
+ * Only the freezes a user sat through. The system slows the timers of a
5
+ * window behind others or out of view, sometimes to one a second, and the
6
+ * stall monitor cannot tell that from a freeze: a timer back a second late
7
+ * reads the same either way. A window that has been in front, with focus,
8
+ * since before the stall began ran at full speed, so its late timers are
9
+ * freezes. Anything else is dropped, including a real freeze the user
10
+ * clicked into the window during: missing one beats reporting a false one.
11
+ */
12
+ export function forwardAttendedStalls(
13
+ forward: (event: StallEvent) => void,
14
+ target: Window = window,
15
+ now: () => number = () => performance.now(),
16
+ ): { sink: (event: StallEvent) => void; dispose: () => void } {
17
+ const doc = target.document;
18
+ const attended = () => doc.visibilityState === "visible" && doc.hasFocus();
19
+ // Since when the window has been attended; never while it is not.
20
+ let attendedSince = attended() ? now() : Number.POSITIVE_INFINITY;
21
+ const update = () => {
22
+ if (!attended()) attendedSince = Number.POSITIVE_INFINITY;
23
+ else if (attendedSince === Number.POSITIVE_INFINITY) attendedSince = now();
24
+ };
25
+ target.addEventListener("focus", update);
26
+ target.addEventListener("blur", update);
27
+ doc.addEventListener("visibilitychange", update);
28
+ return {
29
+ sink: (event) => {
30
+ if (attended() && attendedSince <= now() - event.blockedMs) forward(event);
31
+ },
32
+ dispose: () => {
33
+ target.removeEventListener("focus", update);
34
+ target.removeEventListener("blur", update);
35
+ doc.removeEventListener("visibilitychange", update);
36
+ },
37
+ };
38
+ }