@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
@@ -6,10 +6,13 @@ import { t } from "../../i18n";
6
6
  import { DialogFrame, fitDialogWidth } from "./frame";
7
7
  import { ListView, listCursorMove, type ListViewItem } from "./list-view";
8
8
  import { clampIndex } from "../../utils/math";
9
+ import { withChoiceGroupHeadings } from "./choice-groups";
9
10
 
10
11
  export interface ChoiceDialogChoice {
11
12
  id: string;
12
13
  label: string;
14
+ /** Heading for consecutive choices in the same group. */
15
+ group?: string;
13
16
  description?: string;
14
17
  detail?: string;
15
18
  disabled?: boolean;
@@ -31,10 +34,10 @@ function getChoiceDescription(choice: ChoiceDialogChoice | undefined): string {
31
34
  return choice?.description ?? "";
32
35
  }
33
36
 
34
- function getInitialChoiceIndex(choices: ChoiceDialogChoice[], selectedChoiceId: string | undefined): number {
37
+ function getInitialChoiceIndex(choices: ListViewItem[], selectedChoiceId: string | undefined): number {
35
38
  if (!selectedChoiceId) return choices.findIndex((choice) => !choice.disabled);
36
- const selectedIndex = choices.findIndex((choice) => choice.id === selectedChoiceId);
37
- return selectedIndex >= 0 ? selectedIndex : 0;
39
+ const selectedIndex = choices.findIndex((choice) => choice.id === selectedChoiceId && choice.kind !== "heading");
40
+ return selectedIndex >= 0 ? selectedIndex : choices.findIndex((choice) => choice.kind !== "heading");
38
41
  }
39
42
 
40
43
  function choiceDialogWidth(title: string, choices: ChoiceDialogChoice[]): number {
@@ -42,6 +45,7 @@ function choiceDialogWidth(title: string, choices: ChoiceDialogChoice[]): number
42
45
  title.length,
43
46
  ...choices.map((choice) => choice.label.length + (choice.detail ? choice.detail.length + 4 : 0)),
44
47
  ...choices.map((choice) => getChoiceDescription(choice).length),
48
+ ...choices.map((choice) => choice.group?.length ?? 0),
45
49
  );
46
50
  return Math.max(34, Math.min(76, contentWidth + 4));
47
51
  }
@@ -56,27 +60,28 @@ export function ChoiceDialog({
56
60
  }: ChoiceDialogProps) {
57
61
  const colors = useThemeColors();
58
62
  const desktop = useUiHost().kind === "desktop-web";
59
- const [index, setIndex] = useState(() =>
60
- clampIndex(getInitialChoiceIndex(choices, selectedChoiceId), choices.length, -1)
61
- );
62
- const selectedIndex = clampIndex(index, choices.length, -1);
63
- const selectedChoice = selectedIndex >= 0 ? choices[selectedIndex] : undefined;
64
- // Terminal input can deliver navigation and Enter before React renders.
65
- const cursor = useRef(selectedIndex);
66
- const selectIndex = (next: number) => { cursor.current = next; setIndex(next); };
67
- const items = useMemo<ListViewItem[]>(() => choices.map((choice) => ({
63
+ const items = useMemo<ListViewItem[]>(() => withChoiceGroupHeadings(choices.map((choice) => ({
68
64
  id: choice.id,
69
65
  label: choice.label,
66
+ group: choice.group,
70
67
  description: getChoiceDescription(choice),
71
68
  detail: choice.detail,
72
69
  disabled: choice.disabled,
73
70
  color: choice.color,
74
- })), [choices]);
71
+ }))), [choices]);
72
+ const [index, setIndex] = useState(() =>
73
+ clampIndex(getInitialChoiceIndex(items, selectedChoiceId), items.length, -1)
74
+ );
75
+ const selectedIndex = clampIndex(index, items.length, -1);
76
+ const selectedChoice = selectedIndex >= 0 ? items[selectedIndex] : undefined;
77
+ // Terminal input can deliver navigation and Enter before React renders.
78
+ const cursor = useRef(selectedIndex);
79
+ const selectIndex = (next: number) => { cursor.current = next; setIndex(next); };
75
80
  const width = useMemo(() => choiceDialogWidth(title, choices), [choices, title]);
76
81
 
77
82
  useEffect(() => {
78
- selectIndex(clampIndex(cursor.current, choices.length, -1));
79
- }, [choices.length]);
83
+ selectIndex(clampIndex(cursor.current, items.length, -1));
84
+ }, [items.length]);
80
85
 
81
86
  const activateChoice = (choice: ChoiceDialogChoice | undefined) => {
82
87
  if (!choice || choice.disabled) return;
@@ -87,9 +92,9 @@ export function ChoiceDialog({
87
92
  event.stopPropagation();
88
93
  const move = listCursorMove(event, MAX_VISIBLE_CHOICE_ROWS - 1);
89
94
  if (move) {
90
- selectIndex(move(choices, cursor.current));
95
+ selectIndex(move(items, cursor.current));
91
96
  } else if (event.name === "enter" || event.name === "return") {
92
- activateChoice(choices[clampIndex(cursor.current, choices.length, -1)]);
97
+ activateChoice(items[clampIndex(cursor.current, items.length, -1)]);
93
98
  } else if (event.name === "escape") {
94
99
  resolve("");
95
100
  }
@@ -110,7 +115,7 @@ export function ChoiceDialog({
110
115
  scrollable={items.length > MAX_VISIBLE_CHOICE_ROWS}
111
116
  selectOnHover
112
117
  onSelect={selectIndex}
113
- onActivate={(_, nextIndex) => activateChoice(choices[nextIndex])}
118
+ onActivate={activateChoice}
114
119
  />
115
120
  <Box height={1} />
116
121
  <Text fg={colors.textDim} wrapText>{getChoiceDescription(selectedChoice)}</Text>
@@ -0,0 +1,18 @@
1
+ interface ChoiceGroupHeading {
2
+ id: string;
3
+ label: string;
4
+ kind: "heading";
5
+ disabled: true;
6
+ }
7
+
8
+ /** Label each contiguous group without changing the order or identity of its choices. */
9
+ export function withChoiceGroupHeadings<T extends { group?: string }>(choices: readonly T[]): Array<T | ChoiceGroupHeading> {
10
+ let previousGroup: string | undefined;
11
+ return choices.flatMap((choice, index): Array<T | ChoiceGroupHeading> => {
12
+ const heading = choice.group && choice.group !== previousGroup
13
+ ? { id: `\u0000group:${index}`, label: choice.group, kind: "heading" as const, disabled: true as const }
14
+ : null;
15
+ previousGroup = choice.group;
16
+ return heading ? [heading, choice] : [choice];
17
+ });
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, ScrollBox, Text, TextAttributes, useNativeRenderer } from "../../../../ui";
2
+ import { Box, ScrollBox, Span, Text, TextAttributes, useNativeRenderer } from "../../../../ui";
3
3
  import { hoverBg } from "../../../../theme/colors";
4
4
  import { useThemeColors } from "../../../../theme/theme-context";
5
5
  import { usePaneInstance } from "../../../../state/app/context";
@@ -12,7 +12,6 @@ import { EmptyState } from "../../status";
12
12
  import { observeScrollBoxContentSize, observeScrollBoxViewportSize } from "../../../../renderers/opentui/scrollbox-layout";
13
13
  import {
14
14
  expandTableColumns,
15
- fitTableCellText,
16
15
  fitTableHeaderText,
17
16
  getTableWidth,
18
17
  hasMeaningfulTableHorizontalOverflow,
@@ -30,6 +29,7 @@ import { useFocusOwningPane, useVisibleRangeEmitter } from "../hooks";
30
29
  import { useStableColumns } from "../stable-columns";
31
30
  import {
32
31
  cutColumnText,
32
+ fitColumnCellText,
33
33
  resolveDataTableColumnClips,
34
34
  resolveDataTableVisibleWindow,
35
35
  resolveMoreColumnsHint,
@@ -212,10 +212,8 @@ function OpenTuiDataTableRowInner<
212
212
  const shownWidth = clip ? clip.width : column.width - inset;
213
213
  // Cut text is fitted to what shows; a number the edge cuts stays blank in its own place,
214
214
  // and its cell keeps its background and its press.
215
- const shownText = clip
216
- ? cutColumnText(cell.text, column.align, column.width, clip, (text, width) => fitTableCellText(text, width, column.align))
217
- : shownWidth > 0 ? fitTableCellText(cell.text, shownWidth, column.align) : "";
218
- const contentCut = clip !== null && column.align === "right" && shownText.trim() === "";
215
+ const fitted = fitColumnCellText(cell.text, clip ? column.width : shownWidth, column.align, clip, cell.note);
216
+ const contentCut = clip !== null && column.align === "right" && fitted.text.trim() === "";
219
217
  return (
220
218
  <Box
221
219
  key={column.id}
@@ -255,7 +253,11 @@ function OpenTuiDataTableRowInner<
255
253
  ? colors.selectedText
256
254
  : cell.color ?? colors.text}
257
255
  >
258
- {`${frozen ? " ".repeat(horizontalPadding) : ""}${" ".repeat(inset)}${shownText}${frozen ? " ".repeat(columnGap) : ""}`}
256
+ {fitted.note ? <>
257
+ {`${frozen ? " ".repeat(horizontalPadding) : ""}${" ".repeat(inset)}${fitted.text}`}
258
+ <Span fg={colors.textDim} attributes={TextAttributes.NONE}>{fitted.note}</Span>
259
+ {`${fitted.padding ?? ""}${frozen ? " ".repeat(columnGap) : ""}`}
260
+ </> : `${frozen ? " ".repeat(horizontalPadding) : ""}${" ".repeat(inset)}${fitted.text}${frozen ? " ".repeat(columnGap) : ""}`}
259
261
  </Text>
260
262
  )}
261
263
  </Box>
@@ -1,5 +1,5 @@
1
1
  import { displayWidth } from "../../../../utils/format";
2
- import { tableColumnLeadGap } from "../../table-layout";
2
+ import { fitTableCellText, tableColumnLeadGap } from "../../table-layout";
3
3
 
4
4
  /**
5
5
  * Where the viewport cuts a column: `inset` cells hidden on its left (past the
@@ -46,6 +46,31 @@ export function cutColumnText(
46
46
  return `${" ".repeat(start - clip.inset)}${body}${" ".repeat(shownEnd - end)}`;
47
47
  }
48
48
 
49
+ /** A note uses only spare room: neither the label nor part of the note is cut to fit it. */
50
+ export function fitColumnCellText(
51
+ text: string,
52
+ width: number,
53
+ align: string | undefined,
54
+ clip: DataTableColumnClip | null,
55
+ note?: string,
56
+ ): { text: string; note?: string; padding?: string } {
57
+ const fit = (value: string) => clip
58
+ ? cutColumnText(value, align, width, clip, (body, shown) => fitTableCellText(body, shown, align))
59
+ : width > 0 ? fitTableCellText(value, width, align) : "";
60
+ const combined = note ? `${text} ${note}` : "";
61
+ if (combined && displayWidth(combined) <= (clip?.width ?? width)) {
62
+ const fitted = fit(combined);
63
+ const start = fitted.indexOf(combined);
64
+ // A right-aligned value may fit the visible width but sit beyond its edge.
65
+ if (start >= 0) return {
66
+ text: fitted.slice(0, start + text.length + 1),
67
+ note,
68
+ padding: fitted.slice(start + combined.length),
69
+ };
70
+ }
71
+ return { text: fit(text) };
72
+ }
73
+
49
74
  function sameClip(a: DataTableColumnClip | null, b: DataTableColumnClip | null): boolean {
50
75
  return a === b || (a !== null && b !== null && a.inset === b.inset && a.width === b.width);
51
76
  }
@@ -24,6 +24,8 @@ export type DataTableColumn = Pick<
24
24
 
25
25
  export interface DataTableCell {
26
26
  text: string;
27
+ /** Muted context after `text`, shown whole only when both fit on one line. Does not change the exported value. */
28
+ note?: string;
27
29
  /**
28
30
  * What CSV export writes instead of `text`: the full-precision number behind
29
31
  * it (3.45 for +3.45%, 1234567890 for 1.23B), or a date as an ISO string or
@@ -15,6 +15,7 @@ export interface ListViewItem {
15
15
  description?: string;
16
16
  detail?: string;
17
17
  category?: string;
18
+ /** `heading` draws a muted group label; pair it with `disabled: true`. */
18
19
  kind?: string;
19
20
  right?: string;
20
21
  checked?: boolean;
@@ -148,6 +149,7 @@ function DefaultRow({
148
149
  selected: boolean;
149
150
  }) {
150
151
  const colors = useThemeColors();
152
+ if (item.kind === "heading") return <Text fg={colors.textMuted}>{t(item.label)}</Text>;
151
153
  // The label gives way to the detail: a long label is cut with an ellipsis
152
154
  // instead of wrapping under the row or running into the right column.
153
155
  return (
@@ -289,6 +291,7 @@ export function ListView({
289
291
  const rowHoverBg = hoverBgColor ?? hoverBg(colors);
290
292
  const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : undefined;
291
293
  const activeScrollIndex = scrollIndex ?? selectedIndex;
294
+ const headingAboveSelection = items[activeScrollIndex - 1]?.kind === "heading";
292
295
  const terminalRowHeight = rowHeight ?? 1;
293
296
  const terminalRowGap = rowGap ?? 0;
294
297
  const rowStride = terminalRowHeight + terminalRowGap;
@@ -299,13 +302,14 @@ export function ListView({
299
302
  if (!sb) return;
300
303
  const safeIndex = Math.min(activeScrollIndex, items.length - 1);
301
304
  const viewportH = Math.max(sb.viewport?.height ?? 1, 1);
302
- const rowTop = safeIndex * rowStride;
305
+ const rowTop = Math.max(0, safeIndex - (headingAboveSelection ? 1 : 0)) * rowStride;
306
+ const rowBottom = safeIndex * rowStride + terminalRowHeight;
303
307
  if (rowTop < sb.scrollTop) {
304
308
  sb.scrollTo(rowTop);
305
- } else if (rowTop + terminalRowHeight > sb.scrollTop + viewportH) {
306
- sb.scrollTo(rowTop + terminalRowHeight - viewportH);
309
+ } else if (rowBottom > sb.scrollTop + viewportH) {
310
+ sb.scrollTo(rowBottom - viewportH);
307
311
  }
308
- }, [activeScrollIndex, autoScrollToIndex, items.length, rowStride, scrollable, terminalRowHeight]);
312
+ }, [activeScrollIndex, autoScrollToIndex, headingAboveSelection, items.length, rowStride, scrollable, terminalRowHeight]);
309
313
  useEffect(revealActiveRow, [revealActiveRow]);
310
314
  // A list that opens on a row past the fold has nothing to scroll until its
311
315
  // first computed layout sizes the viewport and the rows.
@@ -12,6 +12,7 @@ import type { QueryBarFilter, QueryBarSearch, QueryBarView } from "./query-bar";
12
12
  interface ChoiceOption {
13
13
  value: string;
14
14
  label: string;
15
+ group?: string;
15
16
  description?: string;
16
17
  disabled?: boolean;
17
18
  color?: string;
@@ -70,6 +71,7 @@ export function useQueryBarKeys({
70
71
  choices={options.map((option) => ({
71
72
  id: option.value,
72
73
  label: option.label,
74
+ group: option.group,
73
75
  description: option.description,
74
76
  disabled: option.disabled,
75
77
  color: option.color,
@@ -417,6 +417,7 @@ export function QueryBar({ width, search, filters = [], view, meta: given, asOf,
417
417
  options: filter.options.map((option) => ({
418
418
  value: option.value,
419
419
  label: option.label,
420
+ group: option.group,
420
421
  description: option.description,
421
422
  disabled: option.disabled,
422
423
  selected: option.value === filter.value,
@@ -18,6 +18,8 @@ export interface SelectControl {
18
18
  export interface SelectButtonOption<T extends string = string> {
19
19
  value: T;
20
20
  label: string;
21
+ /** Heading for a contiguous group of choices; never selectable itself. */
22
+ group?: string;
21
23
  /** Trigger wording when the full label is too wide for a pane header. */
22
24
  short?: string;
23
25
  description?: string;
@@ -130,6 +132,7 @@ export function SelectButton<T extends string = string>({
130
132
  choices={options.map((option) => ({
131
133
  id: option.value,
132
134
  label: option.label,
135
+ group: option.group,
133
136
  description: option.description,
134
137
  disabled: option.disabled,
135
138
  color: option.color,
@@ -158,7 +161,7 @@ export function SelectButton<T extends string = string>({
158
161
  selectRef={(element) => { selectFieldRef.current = element; }}
159
162
  onFocus={onFocus}
160
163
  value={value}
161
- options={options.map((option) => ({ value: option.value, label: option.label, disabled: option.disabled }))}
164
+ options={options.map((option) => ({ value: option.value, label: option.label, group: option.group, disabled: option.disabled }))}
162
165
  disabled={disabled}
163
166
  onChange={selectValue}
164
167
  />
@@ -180,6 +183,7 @@ export function SelectButton<T extends string = string>({
180
183
  options={options.map((option) => ({
181
184
  value: option.value,
182
185
  label: option.label,
186
+ group: option.group,
183
187
  disabled: option.disabled,
184
188
  }))}
185
189
  disabled={disabled}
@@ -9,6 +9,7 @@ export interface SelectFieldHandle {
9
9
 
10
10
  interface SelectFieldOption {
11
11
  label: string;
12
+ group?: string;
12
13
  value: string;
13
14
  description?: string;
14
15
  disabled?: boolean;
@@ -106,18 +106,21 @@ export function getTableWidth(
106
106
  * The columns that fit in `width` cells: whole columns in `dropOrder` (least
107
107
  * needed first) give way until the rest fit, rather than the table's right
108
108
  * edge cutting a value, so a narrow pane drops a name that repeats its ticker
109
- * before it cuts a date. Columns not listed stay at every width. A table that
110
- * scrolls vertically passes its width less the scrollbar's cell.
109
+ * before it cuts a date. An entry that lists several ids drops them together,
110
+ * for columns that mean little apart (a bid's venue and an ask's). Columns not
111
+ * listed stay at every width. A table that scrolls vertically passes its width
112
+ * less the scrollbar's cell.
111
113
  */
112
114
  export function dropColumnsToFit<C extends TableWidthColumn & { id: string }>(
113
115
  columns: readonly C[],
114
116
  width: number,
115
- dropOrder: readonly string[],
117
+ dropOrder: readonly (string | readonly string[])[],
116
118
  ): C[] {
117
119
  let shown = [...columns];
118
- for (const id of dropOrder) {
120
+ for (const step of dropOrder) {
119
121
  if (getTableWidth(shown) <= width) break;
120
- shown = shown.filter((column) => column.id !== id);
122
+ const ids = typeof step === "string" ? [step] : step;
123
+ shown = shown.filter((column) => !ids.includes(column.id));
121
124
  }
122
125
  return shown;
123
126
  }
@@ -1,5 +1,6 @@
1
1
  import { MarketDataCoordinator, getSharedMarketDataCoordinator, setSharedMarketDataCoordinator } from "../market-data/coordinator";
2
2
  import { NewsService, type NewsServiceOptions } from "../news/aggregator";
3
+ import { cloudNewsSession } from "../news/cloud-session";
3
4
  import { getSharedNewsService, setSharedNewsService } from "../news/hooks";
4
5
  import { PluginRegistry } from "../plugins/registry";
5
6
  import { releaseSharedRegistry } from "../plugins/registry/shared";
@@ -61,6 +62,7 @@ export function createAppRuntime({
61
62
  const newsService = new NewsService({
62
63
  connectionHealth: pluginRegistry.connectionHealth,
63
64
  pollIntervalMs: () => Math.max(1, config.refreshIntervalMinutes) * 60_000,
65
+ session: cloudNewsSession,
64
66
  ...newsOptions,
65
67
  });
66
68
  const modules = { pluginRegistry, marketData, newsService };
@@ -27,8 +27,8 @@ export function createAppServices({ config, plugins, externalPlugins }: AppServi
27
27
  provider: {
28
28
  fetchNews: async (query) => (await newsService.load(query)).articles,
29
29
  fetchNewsPage: async (query) => {
30
- const { articles, error, nextCursor } = await newsService.load(query);
31
- return { articles, error, nextCursor };
30
+ const { articles, error, nextCursor, access } = await newsService.load(query);
31
+ return { articles, error, nextCursor, access };
32
32
  },
33
33
  getCachedNews: (query) => newsService.getQueryState(query).articles,
34
34
  },
@@ -20,7 +20,7 @@ import {
20
20
  import { recordOrNull } from "../../utils/guards";
21
21
 
22
22
  const CHART_RANGES = new Set<TimeRange>(TIME_RANGES);
23
- const PRICE_RENDER_MODES = new Set<SeriesStyle>(["area", "line", "candles", "ohlc", "hlc"]);
23
+ const PRICE_RENDER_MODES = new Set<SeriesStyle>(["area", "line", "candles", "hollow", "heikin-ashi", "ohlc", "hlc"]);
24
24
  const LEGACY_INDICATOR_IDS = new Set<LegacyChartIndicatorId>([
25
25
  "volume",
26
26
  "sma20",
@@ -4,6 +4,7 @@ import { hydrateTickerMetadata } from "../tickers/metadata";
4
4
  import type { TickerMetadata, TickerRecord } from "../types/ticker";
5
5
  import { BROWSER_STORAGE_KEYS, SafeJsonStorage, type StorageLike } from "./json-storage";
6
6
  import { isRecord } from "../utils/guards";
7
+ import { runTickerWrite } from "./ticker-writes";
7
8
 
8
9
  type StoredTickers = Record<string, TickerMetadata>;
9
10
 
@@ -39,20 +40,28 @@ export class JsonTickerRepository implements AppTickerRepositoryPort {
39
40
  return metadata ? hydrate(metadata) : null;
40
41
  }
41
42
 
42
- async saveTicker(ticker: TickerRecord): Promise<void> {
43
+ private writeTicker(ticker: TickerRecord): void {
43
44
  const symbol = normalizeSymbol(ticker.metadata.ticker);
44
45
  this.data.set({ ...this.data.get(), [symbol]: ticker.metadata });
45
46
  }
46
47
 
47
- async createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
48
+ saveTicker(ticker: TickerRecord): Promise<void> {
49
+ return runTickerWrite(this, ticker.metadata.ticker, () => this.writeTicker(ticker));
50
+ }
51
+
52
+ createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
48
53
  const ticker = { metadata };
49
- await this.saveTicker(ticker);
50
- return ticker;
54
+ return runTickerWrite(this, metadata.ticker, () => {
55
+ this.writeTicker(ticker);
56
+ return ticker;
57
+ });
51
58
  }
52
59
 
53
- async deleteTicker(symbol: string): Promise<void> {
54
- const tickers = { ...this.data.get() };
55
- delete tickers[normalizeSymbol(symbol)];
56
- this.data.set(tickers);
60
+ deleteTicker(symbol: string): Promise<void> {
61
+ return runTickerWrite(this, symbol, () => {
62
+ const tickers = { ...this.data.get() };
63
+ delete tickers[normalizeSymbol(symbol)];
64
+ this.data.set(tickers);
65
+ });
57
66
  }
58
67
  }
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
2
2
  import type { TickerRecord, TickerMetadata } from "../types/ticker";
3
3
  import type { TickerStore } from "./ticker-store";
4
4
  import { hydrateTickerMetadata } from "../tickers/metadata";
5
+ import { runTickerWrite } from "./ticker-writes";
5
6
 
6
7
  function parseTickerMetadata(json: string): TickerMetadata {
7
8
  return hydrateTickerMetadata(JSON.parse(json) as Record<string, unknown>);
@@ -32,22 +33,28 @@ export class TickerRepository {
32
33
  }
33
34
  }
34
35
 
35
- async saveTicker(ticker: TickerRecord): Promise<void> {
36
- this.tickers.save(ticker.metadata.ticker, ticker.metadata);
36
+ saveTicker(ticker: TickerRecord): Promise<void> {
37
+ return runTickerWrite(this, ticker.metadata.ticker, () => {
38
+ this.tickers.save(ticker.metadata.ticker, ticker.metadata);
39
+ });
37
40
  }
38
41
 
39
42
  /** Saves several tickers in one write. */
40
- async saveTickers(tickers: readonly TickerRecord[]): Promise<void> {
41
- this.tickers.saveMany(tickers.map((ticker) => ({ symbol: ticker.metadata.ticker, metadata: ticker.metadata })));
43
+ saveTickers(tickers: readonly TickerRecord[]): Promise<void> {
44
+ return runTickerWrite(this, tickers.map((ticker) => ticker.metadata.ticker), () => {
45
+ this.tickers.saveMany(tickers.map((ticker) => ({ symbol: ticker.metadata.ticker, metadata: ticker.metadata })));
46
+ });
42
47
  }
43
48
 
44
- async createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
49
+ createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
45
50
  const ticker: TickerRecord = { metadata };
46
- await this.saveTicker(ticker);
47
- return ticker;
51
+ return runTickerWrite(this, metadata.ticker, () => {
52
+ this.tickers.save(metadata.ticker, metadata);
53
+ return ticker;
54
+ });
48
55
  }
49
56
 
50
- async deleteTicker(symbol: string): Promise<void> {
51
- this.tickers.delete(symbol);
57
+ deleteTicker(symbol: string): Promise<void> {
58
+ return runTickerWrite(this, symbol, () => this.tickers.delete(symbol));
52
59
  }
53
60
  }
@@ -0,0 +1,81 @@
1
+ interface TickerWrites {
2
+ revisions: Map<string, number>;
3
+ tails: Map<string, Promise<unknown>>;
4
+ pending: Set<Promise<unknown>>;
5
+ }
6
+
7
+ const repositories = new WeakMap<object, TickerWrites>();
8
+
9
+ function writesFor(repository: object): TickerWrites {
10
+ let writes = repositories.get(repository);
11
+ if (!writes) {
12
+ writes = { revisions: new Map(), tails: new Map(), pending: new Set() };
13
+ repositories.set(repository, writes);
14
+ }
15
+ return writes;
16
+ }
17
+
18
+ function tickerKey(symbol: string): string {
19
+ return symbol.trim().toUpperCase();
20
+ }
21
+
22
+ /** Reserve writes when requested, including ones waiting for an earlier acknowledgement. */
23
+ export function runTickerWrite<Result>(
24
+ repository: object,
25
+ symbols: string | readonly string[],
26
+ write: () => Result | PromiseLike<Result>,
27
+ ): Promise<Result> {
28
+ const writes = writesFor(repository);
29
+ const keys = [...new Set((typeof symbols === "string" ? [symbols] : symbols).map(tickerKey))];
30
+ const previous = [...new Set(keys.flatMap((key) => {
31
+ const tail = writes.tails.get(key);
32
+ return tail ? [tail] : [];
33
+ }))];
34
+ let resolve!: (value: Result | PromiseLike<Result>) => void;
35
+ let reject!: (error: unknown) => void;
36
+ const result = new Promise<Result>((accept, fail) => {
37
+ resolve = accept;
38
+ reject = fail;
39
+ });
40
+ for (const key of keys) {
41
+ writes.revisions.set(key, (writes.revisions.get(key) ?? 0) + 1);
42
+ writes.tails.set(key, result);
43
+ }
44
+ writes.pending.add(result);
45
+ const finished = () => {
46
+ writes.pending.delete(result);
47
+ for (const key of keys) {
48
+ if (writes.tails.get(key) === result) writes.tails.delete(key);
49
+ }
50
+ };
51
+ void result.then(finished, finished);
52
+ const start = () => {
53
+ try {
54
+ resolve(write());
55
+ } catch (error) {
56
+ reject(error);
57
+ }
58
+ };
59
+ if (previous.length) void Promise.allSettled(previous).then(start);
60
+ else start();
61
+ return result;
62
+ }
63
+
64
+ /** Changes immediately when a save or deletion is requested, before its acknowledgement. */
65
+ export function tickerWriteRevision(repository: object, symbol: string): number {
66
+ return repositories.get(repository)?.revisions.get(tickerKey(symbol)) ?? 0;
67
+ }
68
+
69
+ /** Whether a write arrived after a caller last waited for acknowledgements. */
70
+ export function hasPendingTickerWrites(repository: object): boolean {
71
+ return (repositories.get(repository)?.pending.size ?? 0) > 0;
72
+ }
73
+
74
+ /** Let save-await callers publish their edits before cleanup reads the current rows. */
75
+ export async function settleTickerWrites(repository: object): Promise<void> {
76
+ const writes = writesFor(repository);
77
+ do {
78
+ // Each writer handles its own failure; cleanup still needs the last published row.
79
+ await Promise.allSettled([...writes.pending]);
80
+ } while (writes.pending.size);
81
+ }