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
@@ -0,0 +1,666 @@
1
+ import { commandBarResultsFromNodes } from "../../../../remote/command-bar";
2
+ import { findCommandBarResult, findMatchingUiNode } from "../../../../remote/matching";
3
+ import type {
4
+ RemoteControlRequest,
5
+ RemoteControlResponse,
6
+ RemoteJsonPatchOperation,
7
+ RemoteMarketDataRequest,
8
+ RemoteUiNodeSnapshot,
9
+ } from "../../../../remote/types";
10
+ import type { AssistantDecision, TerminalRelayGrants } from "./grants";
11
+ import {
12
+ isKnownRemoteResource,
13
+ resolveTerminalRelayTool,
14
+ terminalRelayToolTitle,
15
+ type TerminalRelayToolKind,
16
+ } from "./manifest";
17
+ import {
18
+ LOCAL_UI_ACTIONS,
19
+ NAVIGATION_RESULT_KINDS,
20
+ operationPolicy,
21
+ patchPolicy,
22
+ } from "./policy";
23
+ import { REDACTED, redactConfig, redactRelayResult, redactResource } from "./redact";
24
+ import { keyArgumentLines, shortValue, type CallSummary } from "./summary";
25
+
26
+ /**
27
+ * Runs the tool calls Gloom Cloud relays from a remote assistant, in this
28
+ * app, and decides on its own whether each may run: the server's word is
29
+ * never enough.
30
+ *
31
+ * 1. The tool must be one this app generated (manifest.ts).
32
+ * 2. The assistant must be approved for this terminal (one prompt each).
33
+ * 3. A call under the `confirm` policy needs the person's Allow for that
34
+ * call, given in this app within 60 s. Nothing the server sends counts.
35
+ * 4. Right before running, the call must still be live: not cancelled by the
36
+ * server, not revoked, not from a closed connection, not answered.
37
+ *
38
+ * Waiting for the person and running the call are separate budgets: up to
39
+ * 60 s for an answer, then up to 15 s for the operation.
40
+ */
41
+
42
+ export const RELAY_PROMPT_SCOPE = "terminal-relay-prompt";
43
+ export const CONFIRM_WINDOW_MS = 60_000;
44
+ const EXECUTION_BUDGET_MS = 15_000;
45
+ /** Answers the server cannot take (a call frame tens of megabytes long) are cut to fit. */
46
+ const MAX_RESULT_CHARS = 900_000;
47
+ const SEEN_CALL_TTL_MS = 10 * 60_000;
48
+ const MAX_SEEN_CALLS = 2_000;
49
+ const MAX_CALL_ID_LENGTH = 64;
50
+
51
+ export interface RelayCaller {
52
+ id: string;
53
+ name: string;
54
+ }
55
+
56
+ export type ApprovalAnswer = Exclude<AssistantDecision, never> | "timeout";
57
+ export type ConfirmationAnswer = "allow" | "deny" | "timeout";
58
+
59
+ /** The prompts, drawn by the host (prompts.tsx). Each closes when `signal` aborts. */
60
+ export interface RelayPrompts {
61
+ askApproval(caller: RelayCaller, signal: AbortSignal): Promise<ApprovalAnswer>;
62
+ askConfirmation(caller: RelayCaller, summary: CallSummary, signal: AbortSignal): Promise<ConfirmationAnswer>;
63
+ }
64
+
65
+ export type RelayActivity =
66
+ | { type: "acted"; caller: RelayCaller; tool: string; at: number }
67
+ | { type: "revoked"; clientId: string };
68
+
69
+ export interface TerminalRelayEngineOptions {
70
+ handle: (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
71
+ /** Sends one frame on the socket the engine was last connected to. */
72
+ send: (frame: Record<string, unknown>) => boolean;
73
+ grants: Pick<TerminalRelayGrants, "decision" | "decide" | "forget" | "markUsed">;
74
+ prompts: RelayPrompts;
75
+ onActivity?: (activity: RelayActivity) => void;
76
+ now?: () => number;
77
+ confirmWindowMs?: number;
78
+ executionBudgetMs?: number;
79
+ }
80
+
81
+ type ErrorCode =
82
+ | "not_allowed"
83
+ | "denied"
84
+ | "confirmation_timeout"
85
+ | "invalid_input"
86
+ | "app_error"
87
+ | "timeout"
88
+ | "revoked";
89
+
90
+ class RelayRefusal extends Error {
91
+ constructor(readonly code: ErrorCode, message: string) {
92
+ super(message);
93
+ }
94
+ }
95
+
96
+ interface LiveCall {
97
+ id: string;
98
+ caller: RelayCaller;
99
+ generation: number;
100
+ controller: AbortController;
101
+ }
102
+
103
+ interface PlannedCall {
104
+ request: RemoteControlRequest | null;
105
+ binding: TerminalRelayToolKind;
106
+ confirm: CallSummary | null;
107
+ /** Re-reads the target right before running; false when it changed while the person decided. */
108
+ stillSame?: () => Promise<boolean>;
109
+ /** Refuses a semantic action aimed at the relay's own prompt. */
110
+ targetsPrompt?: () => Promise<boolean>;
111
+ }
112
+
113
+ function isRecord(value: unknown): value is Record<string, unknown> {
114
+ return !!value && typeof value === "object" && !Array.isArray(value);
115
+ }
116
+
117
+ function asItems(value: unknown): unknown[] {
118
+ return Array.isArray(value) ? value : [value];
119
+ }
120
+
121
+ const DATA_OPERATIONS = new Set<RemoteMarketDataRequest["operation"]>([
122
+ "search",
123
+ "quote",
124
+ "financials",
125
+ "secFilings",
126
+ "holders",
127
+ "analystResearch",
128
+ "corporateActions",
129
+ "earningsCalendar",
130
+ ]);
131
+ const DATA_FIELDS = new Set(["operation", "query", "symbol", "exchange", "count", "symbols"]);
132
+
133
+ /**
134
+ * A market data read built field by field from what the tool allows. Relay
135
+ * input is never spread into a request: a `type` or `requests` key in it must
136
+ * not turn a read into a call or a batch.
137
+ */
138
+ function dataRequest(input: Record<string, unknown>): RemoteMarketDataRequest {
139
+ const unknown = Object.keys(input).filter((key) => !DATA_FIELDS.has(key));
140
+ if (unknown.length > 0) {
141
+ throw new RelayRefusal("invalid_input", `market_data takes only ${[...DATA_FIELDS].join(", ")}; got ${unknown.join(", ")}.`);
142
+ }
143
+ const operation = input.operation;
144
+ if (typeof operation !== "string" || !DATA_OPERATIONS.has(operation as RemoteMarketDataRequest["operation"])) {
145
+ throw new RelayRefusal("invalid_input", `operation must be one of ${[...DATA_OPERATIONS].join(", ")}.`);
146
+ }
147
+ const text = (key: string): string | undefined => {
148
+ const value = input[key];
149
+ if (value === undefined) return undefined;
150
+ if (typeof value !== "string") throw new RelayRefusal("invalid_input", `${key} must be a string.`);
151
+ return value;
152
+ };
153
+ const count = input.count;
154
+ if (count !== undefined && (typeof count !== "number" || !Number.isInteger(count) || count < 1)) {
155
+ throw new RelayRefusal("invalid_input", "count must be a positive integer.");
156
+ }
157
+ const symbols = input.symbols;
158
+ if (symbols !== undefined && (!Array.isArray(symbols) || symbols.some((symbol) => typeof symbol !== "string"))) {
159
+ throw new RelayRefusal("invalid_input", "symbols must be a list of strings.");
160
+ }
161
+ const fields = {
162
+ operation,
163
+ query: text("query"),
164
+ symbol: text("symbol"),
165
+ exchange: text("exchange"),
166
+ count: count as number | undefined,
167
+ symbols: symbols as string[] | undefined,
168
+ };
169
+ const defined = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
170
+ // The type goes last, so nothing above can set it.
171
+ return { ...defined, type: "data" } as RemoteMarketDataRequest;
172
+ }
173
+
174
+ function containsRedacted(value: unknown): boolean {
175
+ if (value === REDACTED) return true;
176
+ if (Array.isArray(value)) return value.some(containsRedacted);
177
+ if (isRecord(value)) return Object.values(value).some(containsRedacted);
178
+ return false;
179
+ }
180
+
181
+ /** Cuts the item list until the answer fits what one socket frame should carry. */
182
+ function fitResult(result: Record<string, unknown>): Record<string, unknown> {
183
+ let text = JSON.stringify(result);
184
+ if (text.length <= MAX_RESULT_CHARS || !Array.isArray(result.items)) return result;
185
+ const items = [...result.items];
186
+ while (items.length > 0 && text.length > MAX_RESULT_CHARS) {
187
+ items.splice(Math.max(0, Math.floor(items.length * 0.75)));
188
+ text = JSON.stringify({ ...result, items });
189
+ }
190
+ return { ...result, items, truncated: true };
191
+ }
192
+
193
+ export class TerminalRelayEngine {
194
+ private generation = 0;
195
+ private readonly live = new Map<string, LiveCall>();
196
+ private readonly seen = new Map<string, number>();
197
+ private promptChain: Promise<unknown> = Promise.resolve();
198
+ private openPrompts = 0;
199
+ private readonly now: () => number;
200
+ private readonly confirmWindowMs: number;
201
+ private readonly executionBudgetMs: number;
202
+
203
+ constructor(private readonly options: TerminalRelayEngineOptions) {
204
+ this.now = options.now ?? Date.now;
205
+ this.confirmWindowMs = options.confirmWindowMs ?? CONFIRM_WINDOW_MS;
206
+ this.executionBudgetMs = options.executionBudgetMs ?? EXECUTION_BUDGET_MS;
207
+ }
208
+
209
+ /** A new socket: calls from the previous one can no longer be answered, so none of them runs. */
210
+ connected(): void {
211
+ this.abortAll();
212
+ this.generation += 1;
213
+ }
214
+
215
+ disconnected(): void {
216
+ this.abortAll();
217
+ this.generation += 1;
218
+ }
219
+
220
+ dispose(): void {
221
+ this.disconnected();
222
+ }
223
+
224
+ get promptOpen(): boolean {
225
+ return this.openPrompts > 0;
226
+ }
227
+
228
+ /** One frame of the relay from Gloom Cloud. */
229
+ receive(type: string, data: unknown): void {
230
+ if (type === "terminal.call") {
231
+ void this.handleCall(data);
232
+ } else if (type === "terminal.cancel" && isRecord(data) && typeof data.id === "string") {
233
+ this.live.get(data.id)?.controller.abort();
234
+ this.live.delete(data.id);
235
+ } else if (type === "terminal.revoke" && isRecord(data) && typeof data.clientId === "string") {
236
+ this.revoke(data.clientId);
237
+ }
238
+ }
239
+
240
+ /** The person or Gloom Cloud revoked an assistant: its decision is forgotten and its calls end. */
241
+ revoke(clientId: string): void {
242
+ this.options.grants.forget(clientId);
243
+ for (const call of [...this.live.values()]) {
244
+ if (call.caller.id !== clientId) continue;
245
+ call.controller.abort();
246
+ this.live.delete(call.id);
247
+ this.reply(call, { ok: false, error: { code: "revoked", message: "This assistant's access to the terminal was revoked." } });
248
+ }
249
+ this.options.onActivity?.({ type: "revoked", clientId });
250
+ }
251
+
252
+ private abortAll(): void {
253
+ for (const call of this.live.values()) call.controller.abort();
254
+ this.live.clear();
255
+ }
256
+
257
+ private rememberCall(id: string): boolean {
258
+ const now = this.now();
259
+ for (const [seenId, at] of this.seen) {
260
+ if (now - at < SEEN_CALL_TTL_MS && this.seen.size <= MAX_SEEN_CALLS) break;
261
+ this.seen.delete(seenId);
262
+ }
263
+ if (this.seen.has(id)) return false;
264
+ this.seen.set(id, now);
265
+ return true;
266
+ }
267
+
268
+ private isLive(call: LiveCall): boolean {
269
+ return !call.controller.signal.aborted
270
+ && call.generation === this.generation
271
+ && this.live.get(call.id) === call;
272
+ }
273
+
274
+ private reply(call: LiveCall, body: Record<string, unknown>): void {
275
+ // A call from a closed connection has nobody to answer to.
276
+ if (call.generation !== this.generation) return;
277
+ this.options.send({ type: "terminal.result", id: call.id, ...body });
278
+ }
279
+
280
+ private async handleCall(data: unknown): Promise<void> {
281
+ if (!isRecord(data) || typeof data.id !== "string" || !data.id || data.id.length > MAX_CALL_ID_LENGTH) return;
282
+ // A repeated id is a replay: the first copy is the only one that runs.
283
+ if (!this.rememberCall(data.id)) return;
284
+ const client = isRecord(data.client) ? data.client : null;
285
+ const caller: RelayCaller | null = client && typeof client.id === "string" && typeof client.name === "string"
286
+ ? { id: client.id.slice(0, 120), name: client.name.trim().slice(0, 60) || "An assistant" }
287
+ : null;
288
+ const call: LiveCall = {
289
+ id: data.id,
290
+ caller: caller ?? { id: "", name: "" },
291
+ generation: this.generation,
292
+ controller: new AbortController(),
293
+ };
294
+ this.live.set(call.id, call);
295
+ try {
296
+ if (!caller || !caller.id) throw new RelayRefusal("not_allowed", "The call does not say which assistant sent it.");
297
+ if (typeof data.tool !== "string") throw new RelayRefusal("not_allowed", "The call names no tool.");
298
+ const binding = resolveTerminalRelayTool(data.tool);
299
+ if (!binding) throw new RelayRefusal("not_allowed", `This Gloom version has no tool "${data.tool}".`);
300
+ const input = isRecord(data.input) ? data.input : {};
301
+
302
+ await this.ensureApproved(call);
303
+ const plan = await this.plan(binding, input);
304
+ if (plan.confirm) await this.confirm(call, plan.confirm);
305
+
306
+ // Last check before anything runs.
307
+ if (!this.isLive(call)) return;
308
+ if (this.options.grants.decision(caller.id) === "deny" || this.options.grants.decision(caller.id) === null) {
309
+ throw new RelayRefusal("revoked", "This assistant's access to the terminal was revoked.");
310
+ }
311
+ if (plan.targetsPrompt && (this.promptOpen || await plan.targetsPrompt())) {
312
+ throw new RelayRefusal("not_allowed", "A Gloom confirmation is open; the assistant cannot act on it.");
313
+ }
314
+ if (plan.stillSame && !(await plan.stillSame())) {
315
+ throw new RelayRefusal("not_allowed", "What the call pointed at changed while the person decided, so it did not run. Read the state again and retry.");
316
+ }
317
+ if (!this.isLive(call)) return;
318
+
319
+ const result = await this.execute(binding, plan, input);
320
+ if (!this.isLive(call)) return;
321
+ this.options.grants.markUsed(caller.id, this.now());
322
+ this.options.onActivity?.({ type: "acted", caller, tool: data.tool, at: this.now() });
323
+ this.reply(call, { ok: true, data: redactRelayResult(fitResult({ asOf: new Date(this.now()).toISOString(), ...result })) });
324
+ } catch (error) {
325
+ if (!this.isLive(call)) return;
326
+ const refusal = error instanceof RelayRefusal
327
+ ? error
328
+ : new RelayRefusal("app_error", error instanceof Error ? error.message : String(error));
329
+ this.reply(call, { ok: false, error: { code: refusal.code, message: refusal.message } });
330
+ } finally {
331
+ if (this.live.get(call.id) === call) this.live.delete(call.id);
332
+ }
333
+ }
334
+
335
+ /** Tells Gloom Cloud the call waits for the person, so it holds the call instead of timing out. */
336
+ private announceWaiting(call: LiveCall, kind: "approval" | "confirmation", expiresAt: number): void {
337
+ if (call.generation !== this.generation) return;
338
+ this.options.send({ type: "terminal.waiting", id: call.id, prompt: kind, expiresAt: new Date(expiresAt).toISOString() });
339
+ }
340
+
341
+ /**
342
+ * One prompt at a time; each keeps its own 60 s window from when it opens.
343
+ * Aborting the call (a cancel, a revoke, a lost connection) closes it at once.
344
+ */
345
+ private prompt<T>(call: LiveCall, kind: "approval" | "confirmation", ask: (signal: AbortSignal) => Promise<T>): Promise<T | "timeout"> {
346
+ this.announceWaiting(call, kind, this.now() + this.confirmWindowMs);
347
+ const run = async (): Promise<T | "timeout"> => {
348
+ if (!this.isLive(call)) throw new RelayRefusal("app_error", "cancelled");
349
+ this.announceWaiting(call, kind, this.now() + this.confirmWindowMs);
350
+ const window = new AbortController();
351
+ const onAbort = () => window.abort();
352
+ call.controller.signal.addEventListener("abort", onAbort, { once: true });
353
+ let timer: ReturnType<typeof setTimeout> | null = null;
354
+ this.openPrompts += 1;
355
+ try {
356
+ return await Promise.race([
357
+ ask(window.signal),
358
+ new Promise<"timeout">((resolve) => {
359
+ timer = setTimeout(() => resolve("timeout"), this.confirmWindowMs);
360
+ window.signal.addEventListener("abort", () => resolve("timeout"), { once: true });
361
+ }),
362
+ ]);
363
+ } finally {
364
+ this.openPrompts -= 1;
365
+ if (timer) clearTimeout(timer);
366
+ window.abort();
367
+ call.controller.signal.removeEventListener("abort", onAbort);
368
+ }
369
+ };
370
+ const result = this.promptChain.then(run, run);
371
+ this.promptChain = result.catch(() => {});
372
+ return result;
373
+ }
374
+
375
+ private async ensureApproved(call: LiveCall): Promise<void> {
376
+ const decision = this.options.grants.decision(call.caller.id);
377
+ if (decision === "always" || decision === "session") return;
378
+ if (decision === "deny") {
379
+ throw new RelayRefusal("denied", `The person declined to let ${call.caller.name} control this terminal. They can change that in Account Management, Agents.`);
380
+ }
381
+ const answer = await this.prompt(call, "approval", (signal) => this.options.prompts.askApproval(call.caller, signal));
382
+ if (!this.isLive(call)) throw new RelayRefusal("app_error", "cancelled");
383
+ if (answer === "timeout") {
384
+ throw new RelayRefusal("confirmation_timeout", `Nobody answered the request to let ${call.caller.name} control this terminal within ${Math.round(this.confirmWindowMs / 1_000)} s, so nothing ran.`);
385
+ }
386
+ // A decision made while this prompt waited (a revoke) does not get overwritten.
387
+ this.options.grants.decide(call.caller.id, call.caller.name, answer, this.now());
388
+ if (answer === "deny") {
389
+ throw new RelayRefusal("denied", `The person declined to let ${call.caller.name} control this terminal.`);
390
+ }
391
+ }
392
+
393
+ private async confirm(call: LiveCall, summary: CallSummary): Promise<void> {
394
+ const answer = await this.prompt(call, "confirmation", (signal) => this.options.prompts.askConfirmation(call.caller, summary, signal));
395
+ if (!this.isLive(call)) throw new RelayRefusal("app_error", "cancelled");
396
+ if (answer === "timeout") {
397
+ throw new RelayRefusal("confirmation_timeout", `The person did not confirm "${summary.title}" within ${Math.round(this.confirmWindowMs / 1_000)} s, so it did not run.`);
398
+ }
399
+ if (answer !== "allow") throw new RelayRefusal("denied", `The person denied "${summary.title}". It did not run.`);
400
+ }
401
+
402
+ private async read(resource: string): Promise<unknown> {
403
+ const response = await this.withBudget(this.options.handle({ type: "get", resource }));
404
+ if (!response.ok) throw new Error(response.error.message);
405
+ return response.data;
406
+ }
407
+
408
+ private async uiNodes(): Promise<RemoteUiNodeSnapshot[]> {
409
+ const nodes = await this.read("ui://tree");
410
+ return Array.isArray(nodes) ? nodes as RemoteUiNodeSnapshot[] : [];
411
+ }
412
+
413
+ /** The request to run and, for the `confirm` cases, what the person is asked. */
414
+ private async plan(binding: TerminalRelayToolKind, input: Record<string, unknown>): Promise<PlannedCall> {
415
+ switch (binding.kind) {
416
+ case "snapshot":
417
+ case "pane-content":
418
+ return { request: null, binding, confirm: null };
419
+ case "resource": {
420
+ const resource = typeof input.resource === "string" ? input.resource : "";
421
+ if (!isKnownRemoteResource(resource)) throw new RelayRefusal("invalid_input", `Unknown resource "${resource}".`);
422
+ return { request: { type: "get", resource }, binding, confirm: null };
423
+ }
424
+ case "data":
425
+ return { request: dataRequest(input), binding, confirm: null };
426
+ case "patch": {
427
+ const resource = typeof input.resource === "string" ? input.resource : "";
428
+ if (!Array.isArray(input.patch)) throw new RelayRefusal("invalid_input", "patch must be a JSON Patch array.");
429
+ if (containsRedacted(input.patch)) {
430
+ throw new RelayRefusal("invalid_input", "The patch writes a redacted placeholder back; credentials cannot be changed remotely.");
431
+ }
432
+ const request: RemoteControlRequest = {
433
+ type: "patch",
434
+ resource,
435
+ patch: input.patch as RemoteJsonPatchOperation[],
436
+ ...(typeof input.expectRev === "string" ? { expectRev: input.expectRev } : {}),
437
+ include: [],
438
+ };
439
+ const policy = patchPolicy(resource).policy;
440
+ return {
441
+ request,
442
+ binding,
443
+ confirm: policy === "allow" ? null : {
444
+ title: "Change the app configuration",
445
+ lines: (input.patch as unknown[]).slice(0, 8).map((entry) => {
446
+ const operation = isRecord(entry) ? entry : {};
447
+ return { label: shortValue(operation.op), value: `${shortValue(operation.path)}${"value" in operation ? ` = ${shortValue(operation.value)}` : ""}` };
448
+ }),
449
+ },
450
+ };
451
+ }
452
+ case "operation":
453
+ return this.planOperation(binding, input);
454
+ }
455
+ }
456
+
457
+ private async planOperation(
458
+ binding: Extract<TerminalRelayToolKind, { kind: "operation" }>,
459
+ input: Record<string, unknown>,
460
+ ): Promise<PlannedCall> {
461
+ const { operation } = binding;
462
+ const request: RemoteControlRequest = { type: "call", operation, input, include: [] };
463
+ const { policy } = operationPolicy(operation);
464
+ if (policy === "allow") return { request, binding, confirm: null };
465
+
466
+ if (policy === "local-ui") {
467
+ const action = typeof input.action === "string" && input.action ? input.action : "press";
468
+ const find = async () => {
469
+ const nodes = await this.uiNodes();
470
+ return operation === "ui.invoke"
471
+ ? nodes.find((node) => node.id === input.nodeId)
472
+ : findMatchingUiNode(nodes, input);
473
+ };
474
+ const target = await find();
475
+ const targetsPrompt = async () => (await find())?.metadata?.scope === RELAY_PROMPT_SCOPE;
476
+ if (target?.metadata?.scope === RELAY_PROMPT_SCOPE || this.promptOpen) {
477
+ throw new RelayRefusal("not_allowed", "A Gloom confirmation is open; the assistant cannot act on it.");
478
+ }
479
+ if (LOCAL_UI_ACTIONS.has(action)) return { request, binding, confirm: null, targetsPrompt };
480
+ const fingerprint = target ? `${target.id}|${target.role}|${target.label ?? ""}` : "";
481
+ return {
482
+ request,
483
+ binding,
484
+ targetsPrompt,
485
+ stillSame: async () => {
486
+ const now = await find();
487
+ return (now ? `${now.id}|${now.role}|${now.label ?? ""}` : "") === fingerprint;
488
+ },
489
+ confirm: {
490
+ title: `${action === "press" ? "Press" : `Run "${action}" on`} ${target?.label ? `"${shortValue(target.label)}"` : "a control"}`,
491
+ lines: [
492
+ { label: "Control", value: target ? `${target.label ? shortValue(target.label) : "unlabelled"} (${target.role})` : "none visible yet" },
493
+ { label: "Action", value: action },
494
+ ...(input.input !== undefined ? [{ label: "Input", value: shortValue(input.input) }] : []),
495
+ ],
496
+ },
497
+ };
498
+ }
499
+
500
+ if (policy === "navigation") {
501
+ const find = async () => findCommandBarResult(commandBarResultsFromNodes(await this.uiNodes()), input);
502
+ const result = await find();
503
+ if (!result) throw new RelayRefusal("invalid_input", "No matching command-bar result is visible.");
504
+ const kind = typeof result.kind === "string" ? result.kind : "";
505
+ if (NAVIGATION_RESULT_KINDS.has(kind)) return { request, binding, confirm: null };
506
+ const fingerprint = `${shortValue(result.label)}|${kind}|${shortValue(result.itemId)}`;
507
+ return {
508
+ request,
509
+ binding,
510
+ stillSame: async () => {
511
+ const now = await find();
512
+ return !!now && `${shortValue(now.label)}|${typeof now.kind === "string" ? now.kind : ""}|${shortValue(now.itemId)}` === fingerprint;
513
+ },
514
+ confirm: {
515
+ title: `Run "${shortValue(result.label)}" from the command bar`,
516
+ lines: [
517
+ { label: "Command", value: shortValue(result.label) },
518
+ ...(result.detail ? [{ label: "Detail", value: shortValue(result.detail) }] : []),
519
+ ],
520
+ },
521
+ };
522
+ }
523
+
524
+ return { request, binding, confirm: await this.operationSummary(operation, input) };
525
+ }
526
+
527
+ private async operationSummary(operation: string, input: Record<string, unknown>): Promise<CallSummary> {
528
+ if (operation === "capability.invoke") {
529
+ const capabilityId = typeof input.capabilityId === "string" ? input.capabilityId : "";
530
+ const operationId = typeof input.operationId === "string" ? input.operationId : "";
531
+ let plugin = capabilityId;
532
+ let operationTitle = operationId;
533
+ try {
534
+ const manifests = await this.read("app://capabilities");
535
+ const manifest = Array.isArray(manifests)
536
+ ? manifests.find((entry) => isRecord(entry) && entry.id === capabilityId) as Record<string, unknown> | undefined
537
+ : undefined;
538
+ if (manifest && typeof manifest.name === "string") plugin = manifest.name;
539
+ const operations = manifest && Array.isArray(manifest.operations) ? manifest.operations : [];
540
+ const described = operations.find((entry) => isRecord(entry) && entry.id === operationId) as Record<string, unknown> | undefined;
541
+ if (described && typeof described.title === "string") operationTitle = described.title;
542
+ } catch {
543
+ // The ids alone still say what runs.
544
+ }
545
+ return {
546
+ title: `${shortValue(operationTitle)} with ${shortValue(plugin)}`,
547
+ lines: [
548
+ { label: "Plugin", value: `${shortValue(plugin)}${plugin !== capabilityId ? ` (${shortValue(capabilityId)})` : ""}` },
549
+ { label: "Operation", value: shortValue(operationId) },
550
+ ...keyArgumentLines(input.payload),
551
+ ],
552
+ };
553
+ }
554
+ if (operation === "layout.delete") {
555
+ let name = `#${String(input.index)}`;
556
+ try {
557
+ const layouts = await this.read("app://layouts");
558
+ const layout = Array.isArray(layouts) && typeof input.index === "number" ? layouts[input.index] : undefined;
559
+ if (isRecord(layout) && typeof layout.name === "string") name = `"${shortValue(layout.name)}"`;
560
+ } catch {
561
+ // The index still identifies it.
562
+ }
563
+ return { title: `Delete the layout ${name}`, lines: [{ label: "Undo", value: "Not available for a deleted layout" }] };
564
+ }
565
+ return {
566
+ title: terminalRelayToolTitle(operation),
567
+ lines: [{ label: "Operation", value: operation }, ...keyArgumentLines(input)],
568
+ };
569
+ }
570
+
571
+ private async withBudget<T>(work: Promise<T>): Promise<T> {
572
+ let timer: ReturnType<typeof setTimeout> | null = null;
573
+ try {
574
+ return await Promise.race([
575
+ work,
576
+ new Promise<never>((_, reject) => {
577
+ timer = setTimeout(
578
+ () => reject(new RelayRefusal("timeout", `The operation did not finish within ${Math.round(this.executionBudgetMs / 1_000)} s.`)),
579
+ this.executionBudgetMs,
580
+ );
581
+ }),
582
+ ]);
583
+ } finally {
584
+ if (timer) clearTimeout(timer);
585
+ }
586
+ }
587
+
588
+ private async run(request: RemoteControlRequest): Promise<RemoteControlResponse & { ok: true }> {
589
+ const response = await this.withBudget(this.options.handle(request));
590
+ if (!response.ok) throw new RelayRefusal("app_error", response.error.message);
591
+ return response;
592
+ }
593
+
594
+ private async execute(
595
+ binding: TerminalRelayToolKind,
596
+ plan: PlannedCall,
597
+ input: Record<string, unknown>,
598
+ ): Promise<Record<string, unknown>> {
599
+ if (binding.kind === "snapshot") return this.withBudget(this.snapshot());
600
+ if (binding.kind === "pane-content") {
601
+ const paneId = typeof input.paneId === "string" ? input.paneId : "";
602
+ if (!paneId) throw new RelayRefusal("invalid_input", "paneId is required.");
603
+ return this.withBudget(this.paneContent(paneId));
604
+ }
605
+ const response = await this.run(plan.request!);
606
+ if (binding.kind === "resource") {
607
+ const resource = (plan.request as { resource: string }).resource;
608
+ return { resource, rev: response.rev ?? null, items: asItems(redactResource(resource, response.data)) };
609
+ }
610
+ if (binding.kind === "patch") {
611
+ const resource = (plan.request as { resource: string }).resource;
612
+ return { resource, rev: response.rev ?? null, items: [resource === "app://config" ? redactConfig(response.data) : response.data] };
613
+ }
614
+ return { items: asItems(response.data ?? null), ...(response.rev ? { rev: response.rev } : {}) };
615
+ }
616
+
617
+ private async snapshot(): Promise<Record<string, unknown>> {
618
+ const snapshot = await this.read("app://snapshot");
619
+ const record = isRecord(snapshot) ? snapshot : {};
620
+ const config = isRecord(record.config) ? record.config : {};
621
+ const layouts = Array.isArray(config.layouts) ? config.layouts : [];
622
+ const activeLayoutIndex = typeof config.activeLayoutIndex === "number" ? config.activeLayoutIndex : 0;
623
+ const panes = Array.isArray(record.panes) ? record.panes : [];
624
+ return {
625
+ rev: record.rev ?? null,
626
+ app: record.app ?? null,
627
+ activeLayout: { index: activeLayoutIndex, name: isRecord(layouts[activeLayoutIndex]) ? layouts[activeLayoutIndex].name ?? null : null },
628
+ layouts: layouts.map((layout, index) => ({ index, name: isRecord(layout) ? layout.name ?? null : null })),
629
+ commandBar: isRecord(record.commandBar)
630
+ ? { open: record.commandBar.open, query: record.commandBar.query, results: Array.isArray(record.commandBar.results) ? record.commandBar.results.length : 0 }
631
+ : null,
632
+ form: record.form ?? null,
633
+ items: panes.map((pane) => {
634
+ if (!isRecord(pane)) return pane;
635
+ const { runtimeState: _runtime, ...rest } = pane;
636
+ return rest;
637
+ }),
638
+ };
639
+ }
640
+
641
+ private async paneContent(paneId: string): Promise<Record<string, unknown>> {
642
+ const panes = await this.read("app://panes");
643
+ const pane = Array.isArray(panes)
644
+ ? panes.find((entry) => isRecord(entry) && (entry.instanceId === paneId || entry.paneId === paneId)) as Record<string, unknown> | undefined
645
+ : undefined;
646
+ if (!pane) throw new RelayRefusal("invalid_input", `No pane "${paneId}" is open. Call terminal.snapshot for the open panes.`);
647
+ const instanceId = String(pane.instanceId);
648
+ let settings: unknown = null;
649
+ try {
650
+ const described = await this.read(`app://pane-settings/${encodeURIComponent(instanceId)}`);
651
+ settings = isRecord(described) ? described.settings ?? null : null;
652
+ } catch {
653
+ // A pane without settings.
654
+ }
655
+ const nodes = (await this.uiNodes()).filter((node) => (
656
+ (node.paneId === instanceId || node.metadata?.paneInstanceId === instanceId)
657
+ // A bare clickable box says nothing about what the pane shows.
658
+ && !(node.role === "box" && !node.label)
659
+ ));
660
+ return {
661
+ pane,
662
+ settings: redactConfig(settings),
663
+ items: nodes.map(({ paneId: _paneId, ...node }) => node),
664
+ };
665
+ }
666
+ }