gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -6,6 +6,7 @@ import type { QueryEntry } from "../result-types";
6
6
  import { resolveCurrencyUnit } from "../../utils/currency-units";
7
7
  import { isFiniteNumber } from "../../utils/guards";
8
8
  import { getCompletedRegularSessionDisplay, getSessionMoveDisplay, type ExtendedSession } from "../market/status";
9
+ import { quoteTradingDay } from "./day-range";
9
10
 
10
11
  const STREAM_FRESHNESS_MS = 2 * 60_000;
11
12
  const STREAM_CONNECTING_GRACE_MS = 15_000;
@@ -39,6 +40,21 @@ export interface ScreenerQuoteOverlayOptions {
39
40
  * two different days.
40
41
  */
41
42
  extendedSessions?: boolean;
43
+ /**
44
+ * Only the regular session, never an extended print, whatever
45
+ * `extendedSessions` says: the live quote while the session trades, then
46
+ * the completed session's close and its move, given as
47
+ * `regularChangePercent`. A quote that cannot say which close that is
48
+ * leaves its row as it was.
49
+ */
50
+ regularSession?: boolean;
51
+ /**
52
+ * With `regularSession`, the completed regular session the rows already
53
+ * are (a New York date) when they were taken outside the session. A quote
54
+ * replaces such a row only with that session's close or a later one, and a
55
+ * live quote only once a later session trades.
56
+ */
57
+ completedSessionDate?: string | null;
42
58
  }
43
59
 
44
60
  export interface ScreenerQuoteFreshness {
@@ -83,14 +99,18 @@ function overlayCurrency(row: ScreenerQuoteRow, quote: Quote): string {
83
99
  * entries map, but most rows' quotes did not move; returning the same row
84
100
  * object for those lets memoized table rows skip the render.
85
101
  */
86
- const overlayCache = new WeakMap<object, { quote: Quote; extended: boolean; row: ScreenerQuoteRow }>();
102
+ const overlayCache = new WeakMap<object, { quote: Quote; basis: string; row: ScreenerQuoteRow }>();
87
103
 
88
104
  export function overlayScreenerQuoteEntries<T extends ScreenerQuoteRow>(
89
105
  rows: readonly T[],
90
106
  entries: ReadonlyMap<string, QueryEntry<Quote>>,
91
107
  options: ScreenerQuoteOverlayOptions = {},
92
108
  ): T[] {
93
- const extended = options.extendedSessions === true;
109
+ const regular = options.regularSession === true;
110
+ const completedSessionDate = regular ? options.completedSessionDate ?? null : null;
111
+ const extended = !regular && options.extendedSessions === true;
112
+ // The cached row is reused only for the same quote under the same options.
113
+ const basis = regular ? `regular:${completedSessionDate ?? ""}` : extended ? "extended" : "plain";
94
114
  return rows.map((row) => {
95
115
  const quote = resolveEntryData(entries.get(quoteKey(row)));
96
116
  if (!quote || !isFiniteNumber(quote.price)) return row;
@@ -102,9 +122,11 @@ export function overlayScreenerQuoteEntries<T extends ScreenerQuoteRow>(
102
122
  return row;
103
123
  }
104
124
  const cached = overlayCache.get(row);
105
- if (cached?.quote === quote && cached.extended === extended) return cached.row as T;
106
- const overlaid = extended ? overlaySessionQuote(row, quote) : overlayQuote(row, quote);
107
- overlayCache.set(row, { quote, extended, row: overlaid });
125
+ if (cached?.quote === quote && cached.basis === basis) return cached.row as T;
126
+ const overlaid = regular
127
+ ? overlayRegularSessionQuote(row, quote, completedSessionDate)
128
+ : extended ? overlaySessionQuote(row, quote) : overlayQuote(row, quote);
129
+ overlayCache.set(row, { quote, basis, row: overlaid });
108
130
  return overlaid;
109
131
  });
110
132
  }
@@ -149,6 +171,35 @@ function overlaySessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote):
149
171
  };
150
172
  }
151
173
 
174
+ function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote, completedSessionDate: string | null): T {
175
+ const state = quote.marketState;
176
+ if (state == null || state === "REGULAR") {
177
+ // On a board of a completed session, only a session that trades after it is news.
178
+ if (completedSessionDate) {
179
+ const day = state === "REGULAR" ? quoteTradingDay(quote) : null;
180
+ if (day == null || day <= completedSessionDate) return row;
181
+ }
182
+ return overlayQuote(row, quote);
183
+ }
184
+ const display = getCompletedRegularSessionDisplay(quote);
185
+ const closeDate = quote.regularCloseSessionDate;
186
+ if (
187
+ !display
188
+ || !(isFiniteNumber(display.price) && display.price > 0)
189
+ || !isFiniteNumber(display.changePercent)
190
+ || (completedSessionDate != null && closeDate != null && closeDate < completedSessionDate)
191
+ ) {
192
+ return row;
193
+ }
194
+ return {
195
+ ...overlayQuote(row, quote),
196
+ price: display.price,
197
+ change: isFiniteNumber(display.change) ? display.change : null,
198
+ changePercent: display.changePercent,
199
+ regularChangePercent: display.changePercent,
200
+ };
201
+ }
202
+
152
203
  function freshQuote(
153
204
  entry: QueryEntry<Quote> | undefined,
154
205
  freshness: ScreenerQuoteFreshness,
@@ -22,6 +22,7 @@ export function useAccountManagementKeyboard({
22
22
  draftRef,
23
23
  fieldOrder,
24
24
  focused,
25
+ openAssistants,
25
26
  openPasswordDialog,
26
27
  openPortfolioDialog,
27
28
  openUpgrade,
@@ -38,6 +39,7 @@ export function useAccountManagementKeyboard({
38
39
  /** The active tab's ring, in reading order. */
39
40
  fieldOrder: readonly AccountFieldKey[];
40
41
  focused: boolean;
42
+ openAssistants: () => void;
41
43
  openPasswordDialog: () => void;
42
44
  openPortfolioDialog: () => Promise<void>;
43
45
  openUpgrade: () => void;
@@ -52,6 +54,7 @@ export function useAccountManagementKeyboard({
52
54
  sharedPortfolioId: () => { void openPortfolioDialog(); },
53
55
  passwordAction: openPasswordDialog,
54
56
  upgradeAction: openUpgrade,
57
+ assistantsAction: openAssistants,
55
58
  deleteAccountAction: () => { void deleteAccount(); },
56
59
  emailAlertsOffAction: () => { void turnOffEmailAlerts(); },
57
60
  };
@@ -21,6 +21,7 @@ export type AccountFieldKey =
21
21
  | "chatEmailNotificationsEnabled"
22
22
  | "emailAlertsOffAction"
23
23
  | "upgradeAction"
24
+ | "assistantsAction"
24
25
  | "passwordAction"
25
26
  | "deleteAccountAction";
26
27
 
@@ -1,6 +1,6 @@
1
1
  import { createPaneRequestChannel } from "../shared/pane-request";
2
2
 
3
- export type AccountManagementTab = "profile" | "emails" | "calendar" | "pro" | "teams" | "advanced";
3
+ export type AccountManagementTab = "profile" | "emails" | "calendar" | "pro" | "teams" | "agents" | "advanced";
4
4
 
5
5
  const tabRequests = createPaneRequestChannel<AccountManagementTab>();
6
6
 
@@ -12,6 +12,8 @@ import {
12
12
  type StatItem,
13
13
  } from "../../../components";
14
14
  import { useAppSelector, usePaneAppConfig } from "../../../state/app/context";
15
+ import { useViewport } from "../../../react/input";
16
+ import { displayWidth } from "../../../utils/format";
15
17
  import { useChartQueries, useFxRatesMap } from "../../../market-data/hooks";
16
18
  import { buildPortfolioFinancialsMap } from "../../../market-data/portfolio-financials";
17
19
  import { useLiveTickerFinancialsMap, useSampledValue } from "../../../state/hooks/live-ticker-financials";
@@ -33,7 +35,9 @@ import { apiClient, type AccountProfile, type CloudPricing } from "../../../api-
33
35
  import { chatController } from "../chat/controller";
34
36
  import { SignInWall } from "../cloud/auth-actions";
35
37
  import { loadUpgradeOffer } from "../cloud/upgrade-dialog";
38
+ import { openMcpConnectDialog } from "../cloud/mcp-connect/dialog";
36
39
  import { TeamsAccountTab } from "../cloud/team/acm-tab";
40
+ import { AgentsAccountTab } from "../cloud/terminal-relay/acm-tab";
37
41
  import {
38
42
  CheckboxRow,
39
43
  FieldRow,
@@ -87,6 +91,7 @@ const ACCOUNT_TAB_DEFS: Array<{ label: string; value: AccountManagementTab }> =
87
91
  { label: "Calendar", value: "calendar" },
88
92
  { label: "Pro", value: "pro" },
89
93
  { label: "Teams", value: "teams" },
94
+ { label: "Agents", value: "agents" },
90
95
  { label: "Advanced", value: "advanced" },
91
96
  ];
92
97
 
@@ -114,7 +119,9 @@ const ACCOUNT_TAB_FIELD_ORDER: Record<AccountManagementTab, AccountFieldKey[]> =
114
119
  pro: ["upgradeAction"],
115
120
  // The Teams tab owns its own keyboard handling (a list, not form fields).
116
121
  teams: [],
117
- advanced: ["passwordAction", "deleteAccountAction"],
122
+ // Remote assistants allowed to drive this terminal; a list with its own footer.
123
+ agents: [],
124
+ advanced: ["assistantsAction", "passwordAction", "deleteAccountAction"],
118
125
  };
119
126
 
120
127
  const PROFILE_PREVIEW_SAMPLE_MS = 10_000;
@@ -562,6 +569,12 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
562
569
  }).catch(() => {});
563
570
  }, [busy, dialog]);
564
571
 
572
+ const viewport = useViewport();
573
+ const openAssistants = useCallback(() => {
574
+ setActiveField("assistantsAction");
575
+ void openMcpConnectDialog(dialog, { viewportWidth: viewport.width });
576
+ }, [dialog, viewport.width]);
577
+
565
578
  const openPortfolioPicker = useCallback(async () => {
566
579
  setActiveField("sharedPortfolioId");
567
580
  portfolioSelectRef.current?.open();
@@ -705,8 +718,8 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
705
718
 
706
719
  useAccountManagementFooter({
707
720
  busy,
708
- // The Calendar tab saves nothing and shows its own status.
709
- enabled: activeTab !== "calendar",
721
+ // The Calendar and Agents tabs save nothing and show their own status.
722
+ enabled: activeTab !== "calendar" && activeTab !== "agents",
710
723
  hasSession,
711
724
  message,
712
725
  saveProfile,
@@ -719,7 +732,8 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
719
732
  draftRef,
720
733
  fieldOrder,
721
734
  // Behind the sign-in wall the fields are not there, and Enter is the wall's.
722
- focused: focused && activeTab !== "teams" && activeTab !== "calendar" && (hasSession || apiClient.isSignedIn()),
735
+ focused: focused && activeTab !== "teams" && activeTab !== "calendar" && activeTab !== "agents" && (hasSession || apiClient.isSignedIn()),
736
+ openAssistants,
723
737
  openPasswordDialog,
724
738
  openPortfolioDialog: openPortfolioPicker,
725
739
  openUpgrade,
@@ -927,8 +941,21 @@ export function AccountManagementPane({ focused, width, height }: PaneProps) {
927
941
  {activeTab === "teams" ? (
928
942
  <TeamsAccountTab focused={focused} width={contentWidth} />
929
943
  ) : null}
944
+ {activeTab === "agents" ? (
945
+ <AgentsAccountTab focused={focused} width={contentWidth} />
946
+ ) : null}
930
947
  {activeTab === "advanced" ? (
931
948
  <>
949
+ <Box ref={fieldNodeRef("assistantsAction")} flexDirection="row" gap={1} alignItems="center">
950
+ {/* The only label on the tab, so it takes the room it needs, with the terminal's "> " marker. */}
951
+ <FieldLabel label={t("Assistants (MCP)")} active={activeField === "assistantsAction"} width={displayWidth(t("Assistants (MCP)")) + 3} />
952
+ <Button
953
+ label={t("Connect an AI Assistant")}
954
+ active={activeField === "assistantsAction"}
955
+ onPress={openAssistants}
956
+ disabled={!!busy}
957
+ />
958
+ </Box>
932
959
  <Box flexDirection="row" gap={1}>
933
960
  <Button
934
961
  label={t("Change Password")}
@@ -1,6 +1,7 @@
1
1
  import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { Quote } from "../../../types/financials";
3
3
  import { getActiveQuoteDisplay } from "../../../market-data/market/status";
4
+ import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
4
5
  import type { AlertRule } from "./types";
5
6
 
6
7
  export function normalizeAlertSymbol(value: string | null | undefined): string {
@@ -21,6 +22,8 @@ export async function resolveAlertQuote(
21
22
  exchange = "",
22
23
  ): Promise<Quote> {
23
24
  const quote = await marketData.getQuote(symbol, exchange);
25
+ // A last known price is not the price now: it must neither trigger an alert nor set its baseline.
26
+ if (isQuoteStaleForCurrentSession(quote)) throw new Error(`The quote for "${symbol}" is stale.`);
24
27
  const display = getActiveQuoteDisplay(quote);
25
28
  if (!display || !Number.isFinite(display.price)) {
26
29
  throw new Error(`No quote found for "${symbol}".`);
@@ -11,6 +11,7 @@ import {
11
11
  riskCoverageShortfall,
12
12
  riskCoverageText,
13
13
  RISK_VIEWS,
14
+ riskEvidence,
14
15
  riskPercentile,
15
16
  riskValue,
16
17
  type PortfolioRiskModel,
@@ -39,7 +40,8 @@ function displayRow(row: RiskDisplayRow) {
39
40
  value: riskValue(row),
40
41
  percentile: riskPercentile(row),
41
42
  asOf: row.asOf?.slice(0, 10) ?? null,
42
- detail: row.detail,
43
+ detail: riskEvidence(row),
44
+ ...(row.amount != null ? { amount: row.amount, currency: row.amountCurrency ?? null } : {}),
43
45
  };
44
46
  }
45
47
 
@@ -1,6 +1,6 @@
1
1
  import { portfolioOptionGreeks } from "./risk-options";
2
2
  import type { Portfolio, TickerRecord } from "../../../types/ticker";
3
- import { formatNumber } from "../../../utils/format";
3
+ import { formatCurrency, formatNumber } from "../../../utils/format";
4
4
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
5
5
  import {
6
6
  getPortfolioPositionMetrics,
@@ -97,7 +97,13 @@ export interface RiskDisplayRow {
97
97
  interaction?: number;
98
98
  /** Holdings rows only: why the holding is outside the covered basket. */
99
99
  leftOut?: string;
100
+ /** VaR and expected shortfall only: the loss `value` is on the covered basket's current value (`amountBase`), in `amountCurrency`. */
101
+ amount?: number;
102
+ amountCurrency?: string;
103
+ amountBase?: number;
100
104
  }
105
+ /** The tail-loss rows, which also read in money on the covered basket. */
106
+ const TAIL_ROW_IDS: ReadonlySet<string> = new Set(["var", "es"]);
101
107
  /**
102
108
  * Below half of market value the qualifying holdings are not most of the
103
109
  * account, so a basket estimate would mostly describe something other than it;
@@ -565,12 +571,19 @@ export function buildPortfolioRisk(
565
571
  : null;
566
572
  warnings.push(...(greeks?.warnings ?? []));
567
573
  const rows: Record<RiskView, RiskDisplayRow[]> = {
568
- risk: metrics.map((row) => ({
569
- ...row,
570
- percentile: row.rank.percentile,
571
- // The method (price returns at fixed current weights) is in docs; the footer keeps room for coverage.
572
- detail: `${row.samples} sessions`,
573
- })),
574
+ risk: metrics.map((row) => {
575
+ // VaR and expected shortfall are a one-day loss on the covered basket's current value, in the portfolio currency.
576
+ const tail = TAIL_ROW_IDS.has(row.id) && row.value != null && coverage.coveredValue > 0;
577
+ return {
578
+ ...row,
579
+ percentile: row.rank.percentile,
580
+ // The method (price returns at fixed current weights) is in docs; the footer keeps room for coverage.
581
+ detail: `${row.samples} sessions`,
582
+ ...(tail
583
+ ? { amount: (row.value! / 100) * coverage.coveredValue, amountCurrency: coverage.currency, amountBase: coverage.coveredValue }
584
+ : {}),
585
+ };
586
+ }),
574
587
  factors: factors.map((row) => ({
575
588
  ...row,
576
589
  percentile: row.rank.percentile,
@@ -770,6 +783,22 @@ export const riskValue = (row: RiskDisplayRow) =>
770
783
  ? "left out"
771
784
  : row.value == null
772
785
  ? "--"
773
- : `${row.value.toFixed(2)}${row.unit === "%" ? "%" : ` ${row.unit}`}`;
786
+ : `${row.value.toFixed(2)}${row.unit === "%" ? "%" : ` ${row.unit}`}${riskAmount(row)}`;
787
+ /** "220,476 USD basket": the value a tail loss in money is taken on, or null for a row without one. */
788
+ export function riskAmountBase(row: Pick<RiskDisplayRow, "amountBase" | "amountCurrency">): string | null {
789
+ return row.amountBase != null && Number.isFinite(row.amountBase) && row.amountCurrency
790
+ ? `${formatNumber(row.amountBase, 0)} ${row.amountCurrency} basket`
791
+ : null;
792
+ }
793
+ /** A row's evidence with the basket a tail loss in money is taken on. */
794
+ export function riskEvidence(row: RiskDisplayRow): string {
795
+ const base = riskAmountBase(row);
796
+ return base ? `${row.detail}; of ${base}` : row.detail;
797
+ }
798
+ /** " ($5,735)": a tail loss in money beside its percent. */
799
+ function riskAmount(row: RiskDisplayRow): string {
800
+ if (row.amount == null || !Number.isFinite(row.amount) || !row.amountCurrency) return "";
801
+ return ` (${formatCurrency(row.amount, row.amountCurrency, 0)})`;
802
+ }
774
803
  export const riskPercentile = (row: RiskDisplayRow) =>
775
804
  row.percentile == null ? "--" : String(Math.round(row.percentile));
@@ -56,6 +56,8 @@ import {
56
56
  riskCoverageShortfall,
57
57
  riskCoverageText,
58
58
  riskPercentile,
59
+ riskAmountBase,
60
+ riskEvidence,
59
61
  riskValue,
60
62
  RISK_VIEWS,
61
63
  type PortfolioRiskModel,
@@ -178,7 +180,7 @@ function RiskDetail({
178
180
  detail: row.asOf?.slice(0, 10) ?? "Source date unavailable",
179
181
  },
180
182
  ...(row.percentile != null ? [{ id: "percentile", label: "Pctl 1Y", value: riskPercentile(row) }] : []),
181
- { id: "evidence", label: "Evidence", value: row.detail, wide: true },
183
+ { id: "evidence", label: "Evidence", value: riskEvidence(row), wide: true },
182
184
  ];
183
185
  const statRows = statGridRows(items, width);
184
186
  const points = (row.history ?? []).map((point, index) => ({
@@ -458,7 +460,8 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
458
460
  : [
459
461
  ...(basketBody ? [] : [
460
462
  ...(asOfDate && sharedEvidence ? [sharedEvidence] : []),
461
- model.portfolio.currency,
463
+ // The value the tail losses in money are taken on names the currency too.
464
+ rows.map(riskAmountBase).find(Boolean) ?? model.portfolio.currency,
462
465
  ...(asOfDate ? [asOfDate] : []),
463
466
  ...(EVIDENCE_VIEWS.has(view) && model.evidence ? [model.evidence.source] : []),
464
467
  ]),
@@ -576,7 +579,8 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
576
579
  {
577
580
  id: "value",
578
581
  label: valueHeader,
579
- width: sharedUnit || attributionParts ? Math.max(9, valueHeader.length) : 16,
582
+ // A tail loss reads in money beside its percent: 2.60% ($5,735).
583
+ width: sharedUnit || attributionParts ? Math.max(9, valueHeader.length) : rows.some((row) => row.amount != null) ? 18 : 16,
580
584
  align: "right",
581
585
  },
582
586
  ...(attributionParts
@@ -601,7 +605,7 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
601
605
  ]
602
606
  : []),
603
607
  ],
604
- [width, view, valueHeader, sharedUnit, attributionParts, sharedEvidence, showPercentile, sharedDate],
608
+ [width, view, valueHeader, sharedUnit, attributionParts, sharedEvidence, showPercentile, sharedDate, rows],
605
609
  );
606
610
  const selectView = (value: string) => {
607
611
  setView(value);
@@ -146,7 +146,7 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
146
146
  }), [history.loading, result, view, paneId, symbol]);
147
147
 
148
148
  const { strip: tabStrip, rows: tabRows } = usePaneTabs(symbol ? { tabs: TABS, activeValue: view, onSelect: setView, focused, dense: true } : null);
149
- if (!symbol) return <EmptyState title="Choose a ticker." hint="Open BT with a symbol, for example BT AAPL." />;
149
+ if (!symbol) return <EmptyState title="Select a ticker." hint="Open BT with a symbol, for example BT AAPL." />;
150
150
  const bodyHeight = Math.max(4, height - 1 - tabRows);
151
151
  const wide = width - SUMMARY_WIDTH - 1 >= MIN_WIDE_CHART_COLS;
152
152
  const summaryTable = (tableWidth: number, tableHeight: number, before?: ReactNode) => (
@@ -14,6 +14,7 @@ import { formatCurrency, truncateToDisplayWidth } from "../../../utils/format";
14
14
  import { t, tf } from "../../../i18n";
15
15
  import type { BrokerProfileRow } from "./model";
16
16
  import type { BrokerManagerMessage } from "./pane-actions";
17
+ import { signInEndNote } from "./sign-in-end";
17
18
  import { stateColor } from "./table";
18
19
 
19
20
  export type BrokerEditKey = "label" | "enabled" | string;
@@ -277,6 +278,9 @@ export function BrokerDetailContent({
277
278
  >
278
279
  {detailStatusMessage}
279
280
  </Text>
281
+ {row.signInEndsAt !== null && (
282
+ <Text fg={colors.warning} width={width} wrapText>{signInEndNote(row.signInEndsAt)}</Text>
283
+ )}
280
284
  <Text fg={colors.textMuted}>{tf("Last sync {time}", { time: formatRelativeAge(row.lastSyncedAt) })}</Text>
281
285
  <Text fg={colors.textMuted}>{tf("Status updated {time}", { time: formatRelativeAge(row.updatedAt) })}</Text>
282
286
  <Box height={1} />
@@ -37,6 +37,7 @@ import {
37
37
  type BrokerProfileRow,
38
38
  } from "./model";
39
39
  import { useBrokerManagerActions } from "./pane-actions";
40
+ import { useSignInEnds } from "./sign-in-end";
40
41
  import {
41
42
  buildBrokerColumns,
42
43
  renderBrokerCell,
@@ -78,9 +79,10 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
78
79
 
79
80
  // Signed-in profiles take their broker's name from the connector list.
80
81
  const signedInBrokers = useSyncExternalStore(subscribeSignedInBrokers, getSignedInBrokers, getSignedInBrokers);
82
+ const { ends: signInEnds, reload: reloadSignInEnds } = useSignInEnds(config.brokerInstances);
81
83
  const rows = useMemo(
82
- () => buildBrokerProfileRows(config, adapters, brokerAccounts),
83
- [adapters, brokerAccounts, config, language, signedInBrokers, statusVersion],
84
+ () => buildBrokerProfileRows(config, adapters, brokerAccounts, signInEnds),
85
+ [adapters, brokerAccounts, config, language, signInEnds, signedInBrokers, statusVersion],
84
86
  );
85
87
  const selectedIndex = Math.max(0, rows.findIndex((row) => row.id === selectedId));
86
88
  const selectedRow = rows[selectedIndex] ?? null;
@@ -107,9 +109,11 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
107
109
  if (!editKeys.includes(activeEditKey)) setActiveEditKey(editKeys[0] ?? "label");
108
110
  }, [activeEditKey, editDraft, editKeys]);
109
111
 
112
+ // After a connect or sync, which can renew a sign-in or find it ended.
110
113
  const refreshStatuses = useCallback(() => {
111
114
  setStatusVersion((version) => version + 1);
112
- }, []);
115
+ reloadSignInEnds();
116
+ }, [reloadSignInEnds]);
113
117
 
114
118
  const {
115
119
  busy,
@@ -3,6 +3,7 @@ import type { AppConfig, BrokerInstanceConfig } from "../../../types/config";
3
3
  import type { BrokerAccount } from "../../../types/trading";
4
4
  import { formatCurrency } from "../../../utils/format";
5
5
  import { t } from "../../../i18n";
6
+ import type { SignInEnds } from "./sign-in-end";
6
7
 
7
8
  export type BrokerDisplayState =
8
9
  | "disabled"
@@ -24,6 +25,8 @@ export interface BrokerProfileRow {
24
25
  accountCount: number;
25
26
  accountSummary: string;
26
27
  accountIds: string[];
28
+ /** When the broker ends this profile's sign-in, once Gloom says it is time to connect again. */
29
+ signInEndsAt: number | null;
27
30
  instance: BrokerInstanceConfig;
28
31
  adapter: BrokerAdapter | null;
29
32
  }
@@ -107,6 +110,7 @@ export function buildBrokerProfileRows(
107
110
  config: AppConfig,
108
111
  adapters: ReadonlyMap<string, BrokerAdapter | null>,
109
112
  brokerAccounts: Record<string, BrokerAccount[]>,
113
+ signInEnds: SignInEnds = new Map(),
110
114
  ): BrokerProfileRow[] {
111
115
  return config.brokerInstances.map((instance) => {
112
116
  const adapter = adapters.get(instance.brokerType) ?? null;
@@ -139,6 +143,10 @@ export function buildBrokerProfileRows(
139
143
  accountCount: accounts.length,
140
144
  accountSummary: summarizeBrokerAccounts(accounts),
141
145
  accountIds: accounts.map((account) => account.accountId),
146
+ // Say nothing next to a failure or a profile that does not connect.
147
+ signInEndsAt: state.state === "error" || state.state === "disabled" || state.state === "unavailable"
148
+ ? null
149
+ : signInEnds.get(instance.id) ?? null,
142
150
  instance,
143
151
  adapter,
144
152
  };
@@ -114,8 +114,14 @@ export function useBrokerManagerActions({
114
114
  const broker = signedInBrokerForProfile(selectedRow.instance, selectedRow.brokerName);
115
115
  try {
116
116
  setBusy(t("Connecting…"));
117
- if (!await requestBrokerSignIn(broker)) {
118
- setMessage(infoMessage(tf("{broker} was not connected.", { broker: broker.name })));
117
+ // A connected broker is connected again only by a new sign-in: this renews it before the broker ends it.
118
+ // A row that says when the sign-in ends is renewable before this session's first sync too.
119
+ const renew = selectedRow.state === "connected" || selectedRow.signInEndsAt !== null;
120
+ if (!await requestBrokerSignIn(broker, { renew })) {
121
+ // Backing out of a renewal leaves the broker connected, for now.
122
+ setMessage(infoMessage(renew
123
+ ? tf("{broker} sign-in was not renewed.", { broker: broker.name })
124
+ : tf("{broker} was not connected.", { broker: broker.name })));
119
125
  return;
120
126
  }
121
127
  await syncBrokerInstance(selectedRow.id);
@@ -0,0 +1,82 @@
1
+ import { useCallback, useMemo, useSyncExternalStore } from "react";
2
+ import { apiClient } from "../../../api-client";
3
+ import { fetchSignedInBrokerConnection, type SignedInBrokerConnection } from "../../../brokers/signed-in/client";
4
+ import { isSignedInBrokerProfile, signedInBrokerId } from "../../../brokers/signed-in/profile";
5
+ import { tf } from "../../../i18n";
6
+ import { useAsyncResource } from "../../../react/async-resource";
7
+ import type { BrokerInstanceConfig } from "../../../types/config";
8
+ import { formatDetailDate, formatShortDate } from "../../../utils/datetime-format";
9
+
10
+ /**
11
+ * Some brokers end a sign-in on their own clock, however often it was
12
+ * refreshed, and Gloom flags it a day before. Only that case shows anything: a
13
+ * server that predates the flag, or a sign-in with no end in sight, adds nothing.
14
+ */
15
+ export type SignInEnds = ReadonlyMap<string, number>;
16
+
17
+ /** When the broker ends the sign-in, if Gloom says it is time to connect again. */
18
+ export function signInEndOf(connection: Pick<SignedInBrokerConnection, "expiresAt" | "expiresSoon">): number | null {
19
+ if (connection.expiresSoon !== true || !connection.expiresAt) return null;
20
+ const endsAt = Date.parse(connection.expiresAt);
21
+ return Number.isFinite(endsAt) ? endsAt : null;
22
+ }
23
+
24
+ /** The profile row's status, in the place of "Connected": "Ends Oct 10". */
25
+ export function signInEndStatus(endsAt: number, now = Date.now()): string {
26
+ const date = formatShortDate(endsAt, { year: false });
27
+ return endsAt > now ? tf("Ends {date}", { date }) : tf("Ended {date}", { date });
28
+ }
29
+
30
+ /** The profile detail's note: "Sign-in ends Oct 10, 2026 at 3:00 PM". */
31
+ export function signInEndNote(endsAt: number, now = Date.now()): string {
32
+ const when = formatDetailDate(endsAt);
33
+ return endsAt > now ? tf("Sign-in ends {when}", { when }) : tf("Sign-in ended {when}", { when });
34
+ }
35
+
36
+ /**
37
+ * When each signed-in profile's sign-in ends, by instance id, for the profiles
38
+ * whose broker is about to end it. Read from Gloom when the pane opens and
39
+ * after a connect or sync; a failed read is the same as nothing to say.
40
+ */
41
+ export function useSignInEnds(
42
+ instances: readonly BrokerInstanceConfig[],
43
+ ): { ends: SignInEnds; reload: () => void } {
44
+ const profiles = useMemo(
45
+ () => instances.flatMap((instance) => isSignedInBrokerProfile(instance) && instance.enabled !== false
46
+ ? [{ id: instance.id, broker: signedInBrokerId(instance) }]
47
+ : []),
48
+ [instances],
49
+ );
50
+ // One read per broker, however many profiles name it.
51
+ const brokers = useMemo(() => [...new Set(profiles.map((profile) => profile.broker).filter(Boolean))].sort(), [profiles]);
52
+ const brokerKey = brokers.join(",");
53
+ const signedIn = useSyncExternalStore(
54
+ (listener) => apiClient.subscribeCurrentUser(listener),
55
+ () => apiClient.isSignedIn(),
56
+ () => apiClient.isSignedIn(),
57
+ );
58
+ const loader = useMemo(() => brokerKey && signedIn
59
+ ? async () => {
60
+ const reads = await Promise.all(brokerKey.split(",").map(async (broker) => {
61
+ try {
62
+ return [broker, signInEndOf(await fetchSignedInBrokerConnection(broker))] as const;
63
+ } catch {
64
+ return [broker, null] as const;
65
+ }
66
+ }));
67
+ return new Map(reads);
68
+ }
69
+ : null, [brokerKey, signedIn]);
70
+ const { data, reload } = useAsyncResource(loader, { keepPreviousData: true });
71
+
72
+ const ends = useMemo<SignInEnds>(() => {
73
+ const byInstance = new Map<string, number>();
74
+ for (const profile of profiles) {
75
+ const endsAt = data?.get(profile.broker);
76
+ if (endsAt != null) byInstance.set(profile.id, endsAt);
77
+ }
78
+ return byInstance;
79
+ }, [data, profiles]);
80
+ const refresh = useCallback(() => { void reload(); }, [reload]);
81
+ return { ends, reload: refresh };
82
+ }
@@ -4,6 +4,7 @@ import { colors } from "../../../theme/colors";
4
4
  import { formatRelativeAge } from "../../../utils/datetime-format";
5
5
  import type { BrokerDisplayState, BrokerProfileRow } from "./model";
6
6
  import { t } from "../../../i18n";
7
+ import { signInEndStatus } from "./sign-in-end";
7
8
 
8
9
  type BrokerColumnId = "profile" | "status" | "broker" | "mode" | "accounts" | "updated";
9
10
  export type BrokerColumn = DataTableColumn & { id: BrokerColumnId };
@@ -45,6 +46,10 @@ export function renderBrokerCell(row: BrokerProfileRow, column: BrokerColumn): D
45
46
  attributes: TextAttributes.BOLD,
46
47
  };
47
48
  case "status":
49
+ // A sign-in the broker is about to end replaces "Connected" until it is renewed.
50
+ if (row.signInEndsAt !== null) {
51
+ return { text: signInEndStatus(row.signInEndsAt), color: colors.warning };
52
+ }
48
53
  return {
49
54
  text: row.stateLabel,
50
55
  color: stateColor(row.state),