@openhands/agent-canvas 1.5.1 → 1.6.1

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 (720) hide show
  1. package/README.md +1 -1
  2. package/README.windows.md +2 -2
  3. package/build/assets/{Trans-DqF_OEZB.js → Trans-52HCczKK.js} +1 -1
  4. package/build/assets/{acp-providers-DerAmTyY.js → acp-providers-xACMK9ZP.js} +1 -1
  5. package/build/assets/{active-backend-context-B8rMv0X9.js → active-backend-context-Cd1qcZsb.js} +1 -1
  6. package/build/assets/{add-backend-modal-CxC7BNEc.js → add-backend-modal-5chsfln3.js} +1 -1
  7. package/build/assets/{agent-profiles-settings-BJb8YXR1.js → agent-profiles-settings-BWK4H3kR.js} +1 -1
  8. package/build/assets/agent-server-compatibility-DMY0CihN.js +1 -0
  9. package/build/assets/agent-server-conversation-service.api-CI5-xmIX.js +41 -0
  10. package/build/assets/agent-settings-CMd8-O4f.js +2 -0
  11. package/build/assets/agent-settings-CdrhQJXX.js +1 -0
  12. package/build/assets/{alert-banner-Dh51WURQ.js → alert-banner-CLfWuIAX.js} +1 -1
  13. package/build/assets/api-key-entry-screen-Cc3aC7DZ.js +1 -0
  14. package/build/assets/app-settings-B-DQcBoB.js +1 -0
  15. package/build/assets/{automation-detail-CZsu2CoM.js → automation-detail-BgxBIpyK.js} +2 -2
  16. package/build/assets/automation-export-Dbur8OHa.js +2 -0
  17. package/build/assets/automations-list-BWfjizlp.js +1 -0
  18. package/build/assets/{back-nav-button-DVknCcSP.js → back-nav-button-CaKvsD1E.js} +1 -1
  19. package/build/assets/backend-form-modal-YcfpuBZo.js +1 -0
  20. package/build/assets/{backend-synced-settings-badge-BSfN0AZA.js → backend-synced-settings-badge-BellMmgC.js} +1 -1
  21. package/build/assets/{base-modal-CIF_785P.js → base-modal-Cw7enAHz.js} +1 -1
  22. package/build/assets/{brand-button-B1izLAdR.js → brand-button-BulEFHr3.js} +1 -1
  23. package/build/assets/{browser-DKaeq_sT.js → browser-DfVgUREb.js} +2 -2
  24. package/build/assets/browser-store-CBqI3tNR.js +1 -0
  25. package/build/assets/{browser-tab-qb_ftCxW.js → browser-tab-CmIKQB7S.js} +1 -1
  26. package/build/assets/chat-send-button-BchSSwuD.js +1 -0
  27. package/build/assets/circle-check-BXbQ5nEH.js +1 -0
  28. package/build/assets/{circle-plus-check-toggle-DoFIjVE0.js → circle-plus-check-toggle-BhpPjkh9.js} +1 -1
  29. package/build/assets/client-source-DAUgyJsi.js +1 -0
  30. package/build/assets/cloud-funnel-analytics-B5OeWh4y.js +1 -0
  31. package/build/assets/{color-themes-CHizm1xF.js → color-themes-QKMSVfDU.js} +1 -1
  32. package/build/assets/{combobox-caret-CxZpqoSR.js → combobox-caret-kNZtp53R.js} +1 -1
  33. package/build/assets/{command-menu-oTvKDU3Y.js → command-menu-tUsn23zy.js} +1 -1
  34. package/build/assets/{command-store-D8J5LMW1.js → command-store-B0hrIWMN.js} +1 -1
  35. package/build/assets/{condenser-settings-BGlWn93P.js → condenser-settings-BBizzfIH.js} +1 -1
  36. package/build/assets/{confirmation-modal-DGBWM2Ls.js → confirmation-modal-108xng7X.js} +1 -1
  37. package/build/assets/{context-menu-list-item-6psNyl7X.js → context-menu-list-item-BbKfFuN1.js} +1 -1
  38. package/build/assets/conversation-CTzSrLMq.js +1 -0
  39. package/build/assets/conversation-CgcXNb-Z.js +90 -0
  40. package/build/assets/conversation-panel-bscjGQda.js +1 -0
  41. package/build/assets/{conversation-service.api-_f9GlIEi.js → conversation-service.api-DbBvscJA.js} +1 -1
  42. package/build/assets/conversation-state-store-C5GagZHT.js +1 -0
  43. package/build/assets/{conversation-store-LptbOvKa.js → conversation-store-CyVqoa0d.js} +1 -1
  44. package/build/assets/{conversation-tab-empty-state-CNUmIms2.js → conversation-tab-empty-state-2KYlk5Bh.js} +1 -1
  45. package/build/assets/{copy-to-clipboard-button-CFzH_NOX.js → copy-to-clipboard-button-C54YRPMg.js} +1 -1
  46. package/build/assets/declaration-_yumYLEx.js +1 -0
  47. package/build/assets/{device-verify-WBSjJBLH.js → device-verify-D6FW_KZq.js} +1 -1
  48. package/build/assets/{dropdown-classes-D3wMJlXT.js → dropdown-classes-9uONlOg1.js} +1 -1
  49. package/build/assets/{ellipsis-button-CbNnHlIq.js → ellipsis-button-CoaUrKmQ.js} +1 -1
  50. package/build/assets/{entry.client-DNlPGxsu.js → entry.client-BJSVzw7V.js} +2 -2
  51. package/build/assets/{enum-filter-dropdown-JLYv1t8L.js → enum-filter-dropdown-qQzyjk_q.js} +1 -1
  52. package/build/assets/{environment-switch-overlay-B1PFO8P8.js → environment-switch-overlay-XRWCClLu.js} +1 -1
  53. package/build/assets/extensions-hub-DUp9sWQY.js +1 -0
  54. package/build/assets/extensions-navigation-DmkftjAx.js +1 -0
  55. package/build/assets/file-tUAzhmJ3.js +1 -0
  56. package/build/assets/files-tab-GV5hwiIM.js +1 -0
  57. package/build/assets/files-tab-store-DehyITNC.js +1 -0
  58. package/build/assets/{form-control-classes-CbdN5aO3.js → form-control-classes-BPUnoYyg.js} +1 -1
  59. package/build/assets/git-provider-icon-DDkru3b_.js +1 -0
  60. package/build/assets/highlighted-source-view-CYkPZUxr.js +1 -0
  61. package/build/assets/home-C53WekZ2.js +1 -0
  62. package/build/assets/home-EAwq8ah2.js +1 -0
  63. package/build/assets/index-home-kBdZGKtu.js +1 -0
  64. package/build/assets/install-server-modal-DYk4VP3-.js +1 -0
  65. package/build/assets/{launch-CAGavYl-.js → launch-CIecOy_y.js} +1 -1
  66. package/build/assets/lesson-plan-BSEUwflx.js +4 -0
  67. package/build/assets/llm-not-configured-banner-C8uv1PHT.js +27 -0
  68. package/build/assets/llm-settings-0Bb97NCk.js +1 -0
  69. package/build/assets/llm-settings-D4toJ90k.js +1 -0
  70. package/build/assets/{llm-subscription-service-Casyzgn1.js → llm-subscription-service-BPRw3u3H.js} +1 -1
  71. package/build/assets/{loading-spinner-BcdnT0mN.js → loading-spinner-BCInDEXB.js} +1 -1
  72. package/build/assets/manage-backends-modal-Bon32JYr.js +1 -0
  73. package/build/assets/manifest-ec000ae3.js +1 -0
  74. package/build/assets/markdown-renderer-BlaJOAI2.js +2 -0
  75. package/build/assets/mcp-CFIpOkn2.js +9 -0
  76. package/build/assets/messages-eRPeR4pW.js +40 -0
  77. package/build/assets/{modal-body-Cc2zp0xb.js → modal-body-Bg9UCeoV.js} +1 -1
  78. package/build/assets/{modal-classes-3qT9Ap7Q.js → modal-classes-H1KbJkeC.js} +1 -1
  79. package/build/assets/{modal-close-button-BcIBUdSq.js → modal-close-button-BMLYvo0C.js} +1 -1
  80. package/build/assets/{model-selector-DJrMrFAr.js → model-selector-BqX2RvEr.js} +1 -1
  81. package/build/assets/{navigation-link-D8hfwvxI.js → navigation-link-DoJzTCEz.js} +1 -1
  82. package/build/assets/onboarding-3KO4qE6R.js +1 -0
  83. package/build/assets/onboarding-modal-D4vqkJeT.js +1 -0
  84. package/build/assets/{organization-service.api-vrRNwOma.js → organization-service.api-CvfQugOt.js} +1 -1
  85. package/build/assets/{path-utils-PNfCKQDV.js → path-utils-DRgexxnX.js} +1 -1
  86. package/build/assets/{plan-components-BdZXPyZU.js → plan-components-qkMiGs_-.js} +1 -1
  87. package/build/assets/{planner-tab-ByOw1he8.js → planner-tab-ZrjiirRr.js} +1 -1
  88. package/build/assets/{plugins-management-service-B75jyC56.js → plugins-management-service-9qT-r_37.js} +1 -1
  89. package/build/assets/plus-CzE1b-wl.js +1 -0
  90. package/build/assets/{profiles-service.api-BQTklD5D.js → profiles-service.api-CRVlrQpT.js} +1 -1
  91. package/build/assets/{providers-DfTB2gPd.js → providers-C2DFGBrf.js} +1 -1
  92. package/build/assets/{proxy-B3tCGf0X.js → proxy-DnnDqkwn.js} +1 -1
  93. package/build/assets/puzzle-IHQnD54C.js +1 -0
  94. package/build/assets/recommended-automations-launcher-B97vgHuN.js +58 -0
  95. package/build/assets/refresh-cw-DxbAs3SK.js +1 -0
  96. package/build/assets/root-BbONavTg.css +1 -0
  97. package/build/assets/root-D01rpqZU.js +2 -0
  98. package/build/assets/root-layout-BiHbCIkS.js +2 -0
  99. package/build/assets/{runtime-waiting-state-CnyVhX4T.js → runtime-waiting-state-CbUV9txX.js} +1 -1
  100. package/build/assets/{schema-field-CUwk4wrQ.js → schema-field-B9-9Ta9u.js} +1 -1
  101. package/build/assets/sdk-section-page-DG1s25Pr.js +1 -0
  102. package/build/assets/{sdk-settings-schema-CLR01YIK.js → sdk-settings-schema-B42JFr5Y.js} +1 -1
  103. package/build/assets/{secrets-service-CvyPWJhg.js → secrets-service-DvyS6-mV.js} +1 -1
  104. package/build/assets/secrets-settings-CeSq6cQw.js +1 -0
  105. package/build/assets/{segmented-toggle-kzwZkLaj.js → segmented-toggle-DRVJBNy0.js} +1 -1
  106. package/build/assets/settings-Dzo0dJKM.js +1 -0
  107. package/build/assets/{settings-mQUckT0g.js → settings-HcwfqMjJ.js} +1 -1
  108. package/build/assets/{settings-dropdown-input-Dhx9ErGd.js → settings-dropdown-input-B6RL_mt0.js} +1 -1
  109. package/build/assets/settings-index-uAKHyijw.js +1 -0
  110. package/build/assets/{settings-input-BsgQWS5R.js → settings-input-ZoaAPNQh.js} +1 -1
  111. package/build/assets/{settings-list-classes-DSJJX99U.js → settings-list-classes-Dr8LgITi.js} +1 -1
  112. package/build/assets/settings-modal-BVX2G_IT.js +1 -0
  113. package/build/assets/{settings-service.api-DXk5TdeJ.js → settings-service.api-BY7ncxIz.js} +1 -1
  114. package/build/assets/{settings-switch-Bes8dIlV.js → settings-switch-DUlGnP0B.js} +1 -1
  115. package/build/assets/{settings-utils-DzCVfiR3.js → settings-utils-kWEEvcdv.js} +1 -1
  116. package/build/assets/shared-conversation-CV4Rtdz2.js +1 -0
  117. package/build/assets/{sidebar-layout-Bm7pa3Vk.js → sidebar-layout-BhGooCga.js} +1 -1
  118. package/build/assets/sidebar-mobile-menu-toggle-BntOJphE.js +1 -0
  119. package/build/assets/{sidebar-nav-link-CHqyFXwV.js → sidebar-nav-link-DkA1XWV8.js} +1 -1
  120. package/build/assets/{sidebar-store-uiUXBU1N.js → sidebar-store-CF3b12-m.js} +1 -1
  121. package/build/assets/{skill-card-pill-row-BPYKQ3IN.js → skill-card-pill-row-_gWe1Tws.js} +1 -1
  122. package/build/assets/skill-icon-badge-C-zbaXbD.js +1 -0
  123. package/build/assets/skills-plugins-BwoXKtrR.js +1 -0
  124. package/build/assets/skills-settings-Crq-fZSt.js +2 -0
  125. package/build/assets/{styled-tooltip-B14i_Uac.js → styled-tooltip-CdnCJ67u.js} +1 -1
  126. package/build/assets/syntax-highlighter-CZK2ab-O.js +1 -0
  127. package/build/assets/{task-list-tab-DyfgG4MC.js → task-list-tab-CONOO9iI.js} +1 -1
  128. package/build/assets/telemetry-CC32dJYW.js +2 -0
  129. package/build/assets/{terminal-DRXt2Lr4.js → terminal-CTkDpIU-.js} +1 -1
  130. package/build/assets/{toggle-switch-DX_k418f.js → toggle-switch-yVIzmJLM.js} +1 -1
  131. package/build/assets/{typography-CzfoD9DY.js → typography-BBVEW0un.js} +1 -1
  132. package/build/assets/{use-acp-credential-form-Bm8qXdlc.js → use-acp-credential-form-CIsp8hJe.js} +1 -1
  133. package/build/assets/use-activate-agent-profile-BfcTTJJG.js +1 -0
  134. package/build/assets/{use-active-agent-profile-hI-x-UBa.js → use-active-agent-profile-CCeFtmk9.js} +1 -1
  135. package/build/assets/{use-active-conversation-2qeez40F.js → use-active-conversation-Badxj8w4.js} +1 -1
  136. package/build/assets/{use-agent-profiles-0pr5bLS7.js → use-agent-profiles-BSWVN6hH.js} +1 -1
  137. package/build/assets/{use-agent-state-C6peDXUm.js → use-agent-state-BzRrhoPd.js} +1 -1
  138. package/build/assets/use-backends-health-BsZikyFW.js +1 -0
  139. package/build/assets/{use-can-manage-org-profiles-Ck_bsNbH.js → use-can-manage-org-profiles-Do6mYf6P.js} +1 -1
  140. package/build/assets/{use-cloud-current-user-id-DUCSxNjY.js → use-cloud-current-user-id-Cx3B_0-w.js} +1 -1
  141. package/build/assets/use-config-D9zUVw5b.js +1 -0
  142. package/build/assets/{use-create-conversation-r1JHfKO5.js → use-create-conversation-vvl0kvu7.js} +1 -1
  143. package/build/assets/{use-create-secret-Cf2N11d2.js → use-create-secret-CB7Dvd4s.js} +1 -1
  144. package/build/assets/use-event-store-BlpSUF7N.js +4 -0
  145. package/build/assets/{use-handle-plan-click-CsAW3FlH.js → use-handle-plan-click-DDZkew9p.js} +1 -1
  146. package/build/assets/use-is-creating-conversation-fxSzN778.js +1 -0
  147. package/build/assets/{use-launch-skill-in-chat-DJWEfVc5.js → use-launch-skill-in-chat-D6_n1fs3.js} +1 -1
  148. package/build/assets/use-llm-profiles-CNQm7HYS.js +1 -0
  149. package/build/assets/use-plugins-marketplace-CloojomS.js +1 -0
  150. package/build/assets/use-runtime-is-ready-Bx_4cfar.js +1 -0
  151. package/build/assets/{use-save-agent-profile-RqUGqTv6.js → use-save-agent-profile-Bj7N-1jS.js} +1 -1
  152. package/build/assets/{use-save-settings-Chvzeu2s.js → use-save-settings-BMsb2wk_.js} +1 -1
  153. package/build/assets/use-settings-B9XEmh7i.js +1 -0
  154. package/build/assets/use-settings-nav-items-BGfBS0yL.js +16 -0
  155. package/build/assets/{use-skills-xrVoKYrF.js → use-skills-CSRbvPle.js} +1 -1
  156. package/build/assets/{use-task-list-CfisapBW.js → use-task-list-CTIVLrQA.js} +1 -1
  157. package/build/assets/use-tracking-DjdCssvp.js +1 -0
  158. package/build/assets/{use-user-conversation-CnE4zhOA.js → use-user-conversation-BCpFn-Ei.js} +1 -1
  159. package/build/assets/{useTranslation-Cbf5AGg_.js → useTranslation-CHm_6BRr.js} +1 -1
  160. package/build/assets/{utils-DB9LsAzA.js → utils-CrRpzbM2.js} +1 -1
  161. package/build/assets/vendor-markdown-DzO4uO_0.js +37 -0
  162. package/build/assets/{vendor~browser-wXQ-WZEC.js → vendor~browser-Byfuq5J-.js} +1 -1
  163. package/build/assets/{vendor~browser-tab-CsMb-pta.js → vendor~browser-tab-BLk3qxhw.js} +1 -1
  164. package/build/assets/{vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~dhilvkxv-eGLBu60s.js → vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~dhilvkxv-DTJ_cjR5.js} +2 -2
  165. package/build/assets/{vendor~files-tab-B-5fBohe.js → vendor~files-tab-XmnauIZI.js} +1 -1
  166. package/build/assets/{vendor~launch-_Km3J77E.js → vendor~launch-DeC8QhXQ.js} +1 -1
  167. package/build/assets/{vendor~root-layout~conversation-panel~conversation~shared-conversation-BrguKXCn.js → vendor~root-layout~conversation-panel~conversation~shared-conversation-DDmLzCzm.js} +1 -1
  168. package/build/assets/vendor~root-layout~index-home~home~conversation-panel~conversation-YrfJYTQw.js +1 -0
  169. package/build/assets/{vendor~root-layout~index-home~home~conversation-panel~conversation~launch~skills-settings~a~j2jlu22h-mC0v44WR.js → vendor~root-layout~index-home~home~conversation-panel~conversation~launch~skills-settings~a~j2jlu22h-DoaS51iW.js} +123 -3
  170. package/build/assets/{vendor~root-layout~index-home~home~conversation-panel~conversation~shared-conversation~aler~gfu7qdtz-BOKQkbc6.js → vendor~root-layout~index-home~home~conversation-panel~conversation~shared-conversation~aler~gfu7qdtz-DEYwoy8u.js} +1 -1
  171. package/build/assets/vendor~root-layout~index-home~home~mcp~automations-list~onboarding-modal-DZHLw5Sr.js +1 -0
  172. package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~ftka0fnu-cFNmfAE6.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~ftka0fnu-UJ4BNDJP.js} +1 -1
  173. package/build/assets/{verification-settings-CfkrXoSa.js → verification-settings-uwOfc16F.js} +1 -1
  174. package/build/index.html +4 -4
  175. package/build/locales/ar/openhands.json +38 -4
  176. package/build/locales/ca/openhands.json +38 -4
  177. package/build/locales/de/openhands.json +38 -4
  178. package/build/locales/en/openhands.json +38 -4
  179. package/build/locales/es/openhands.json +38 -4
  180. package/build/locales/fr/openhands.json +38 -4
  181. package/build/locales/it/openhands.json +38 -4
  182. package/build/locales/ja/openhands.json +38 -4
  183. package/build/locales/ko-KR/openhands.json +38 -4
  184. package/build/locales/no/openhands.json +38 -4
  185. package/build/locales/pt/openhands.json +38 -4
  186. package/build/locales/tr/openhands.json +38 -4
  187. package/build/locales/uk/openhands.json +38 -4
  188. package/build/locales/zh-CN/openhands.json +38 -4
  189. package/build/locales/zh-TW/openhands.json +38 -4
  190. package/config/defaults.json +7 -3
  191. package/dist/api/agent-canvas-updates.cjs +2 -0
  192. package/dist/api/agent-canvas-updates.cjs.map +1 -0
  193. package/dist/api/agent-canvas-updates.d.ts +7 -0
  194. package/dist/api/agent-canvas-updates.js +19 -0
  195. package/dist/api/agent-canvas-updates.js.map +1 -0
  196. package/dist/api/agent-server-adapter.cjs +1 -1
  197. package/dist/api/agent-server-adapter.cjs.map +1 -1
  198. package/dist/api/agent-server-adapter.d.ts +3 -0
  199. package/dist/api/agent-server-adapter.js +1 -0
  200. package/dist/api/agent-server-adapter.js.map +1 -1
  201. package/dist/api/agent-server-compatibility.cjs +1 -1
  202. package/dist/api/agent-server-compatibility.cjs.map +1 -1
  203. package/dist/api/agent-server-compatibility.d.ts +5 -0
  204. package/dist/api/agent-server-compatibility.js +24 -15
  205. package/dist/api/agent-server-compatibility.js.map +1 -1
  206. package/dist/api/agent-server-config.cjs +1 -1
  207. package/dist/api/agent-server-config.cjs.map +1 -1
  208. package/dist/api/agent-server-config.js +11 -8
  209. package/dist/api/agent-server-config.js.map +1 -1
  210. package/dist/api/automation-service/automation-service.api.cjs +2 -0
  211. package/dist/api/automation-service/automation-service.api.cjs.map +1 -0
  212. package/dist/api/automation-service/automation-service.api.d.ts +3 -0
  213. package/dist/api/automation-service/automation-service.api.js +283 -0
  214. package/dist/api/automation-service/automation-service.api.js.map +1 -0
  215. package/dist/api/client-source.cjs +1 -1
  216. package/dist/api/client-source.cjs.map +1 -1
  217. package/dist/api/client-source.d.ts +3 -0
  218. package/dist/api/client-source.js +4 -4
  219. package/dist/api/client-source.js.map +1 -1
  220. package/dist/api/cloud/settings-service.api.cjs.map +1 -1
  221. package/dist/api/cloud/settings-service.api.js.map +1 -1
  222. package/dist/api/conversation-service/agent-server-conversation-service.api.cjs +1 -1
  223. package/dist/api/conversation-service/agent-server-conversation-service.api.cjs.map +1 -1
  224. package/dist/api/conversation-service/agent-server-conversation-service.api.js +111 -109
  225. package/dist/api/conversation-service/agent-server-conversation-service.api.js.map +1 -1
  226. package/dist/api/mcp-health/mcp-health-store.cjs +2 -0
  227. package/dist/api/mcp-health/mcp-health-store.cjs.map +1 -0
  228. package/dist/api/mcp-health/mcp-health-store.d.ts +22 -0
  229. package/dist/api/mcp-health/mcp-health-store.js +45 -0
  230. package/dist/api/mcp-health/mcp-health-store.js.map +1 -0
  231. package/dist/api/mcp-health/probe-mcp-server-health.cjs +2 -0
  232. package/dist/api/mcp-health/probe-mcp-server-health.cjs.map +1 -0
  233. package/dist/api/mcp-health/probe-mcp-server-health.d.ts +31 -0
  234. package/dist/api/mcp-health/probe-mcp-server-health.js +57 -0
  235. package/dist/api/mcp-health/probe-mcp-server-health.js.map +1 -0
  236. package/dist/api/mcp-service/mcp-service.api.cjs +1 -1
  237. package/dist/api/mcp-service/mcp-service.api.cjs.map +1 -1
  238. package/dist/api/mcp-service/mcp-service.api.js +66 -46
  239. package/dist/api/mcp-service/mcp-service.api.js.map +1 -1
  240. package/dist/api/plugins-management-service.cjs.map +1 -1
  241. package/dist/api/plugins-management-service.d.ts +6 -1
  242. package/dist/api/plugins-management-service.js.map +1 -1
  243. package/dist/api/plugins-service.d.ts +30 -2
  244. package/dist/api/secrets-service.cjs +1 -1
  245. package/dist/api/secrets-service.cjs.map +1 -1
  246. package/dist/api/secrets-service.d.ts +6 -6
  247. package/dist/api/secrets-service.js +8 -3
  248. package/dist/api/secrets-service.js.map +1 -1
  249. package/dist/api/settings-service/settings-service.api.cjs +1 -1
  250. package/dist/api/settings-service/settings-service.api.cjs.map +1 -1
  251. package/dist/api/settings-service/settings-service.api.d.ts +1 -1
  252. package/dist/api/settings-service/settings-service.api.js +1 -0
  253. package/dist/api/settings-service/settings-service.api.js.map +1 -1
  254. package/dist/components/conversation-events/chat/event-message-components/critic-result-display.cjs +1 -1
  255. package/dist/components/conversation-events/chat/event-message-components/critic-result-display.js +5 -5
  256. package/dist/components/features/backends/backend-form-modal.cjs +1 -1
  257. package/dist/components/features/backends/backend-form-modal.cjs.map +1 -1
  258. package/dist/components/features/backends/backend-form-modal.d.ts +4 -1
  259. package/dist/components/features/backends/backend-form-modal.js +15 -16
  260. package/dist/components/features/backends/backend-form-modal.js.map +1 -1
  261. package/dist/components/features/backends/backend-selector.cjs +1 -1
  262. package/dist/components/features/backends/backend-selector.cjs.map +1 -1
  263. package/dist/components/features/backends/backend-selector.js +103 -102
  264. package/dist/components/features/backends/backend-selector.js.map +1 -1
  265. package/dist/components/features/backends/manage-backends-modal.cjs +1 -1
  266. package/dist/components/features/backends/manage-backends-modal.cjs.map +1 -1
  267. package/dist/components/features/backends/manage-backends-modal.js +85 -73
  268. package/dist/components/features/backends/manage-backends-modal.js.map +1 -1
  269. package/dist/components/features/chat/chat-interface.cjs +2 -2
  270. package/dist/components/features/chat/chat-interface.cjs.map +1 -1
  271. package/dist/components/features/chat/chat-interface.js +131 -129
  272. package/dist/components/features/chat/chat-interface.js.map +1 -1
  273. package/dist/components/features/chat/confirmation-mode-enabled.cjs +1 -1
  274. package/dist/components/features/chat/confirmation-mode-enabled.js +4 -4
  275. package/dist/components/features/chat/skill-install-restart-banner.cjs +2 -0
  276. package/dist/components/features/chat/skill-install-restart-banner.cjs.map +1 -0
  277. package/dist/components/features/chat/skill-install-restart-banner.d.ts +11 -0
  278. package/dist/components/features/chat/skill-install-restart-banner.js +63 -0
  279. package/dist/components/features/chat/skill-install-restart-banner.js.map +1 -0
  280. package/dist/components/features/conversation/right-panel-toggle.cjs +1 -1
  281. package/dist/components/features/conversation/right-panel-toggle.cjs.map +1 -1
  282. package/dist/components/features/conversation/right-panel-toggle.js +1 -1
  283. package/dist/components/features/conversation/right-panel-toggle.js.map +1 -1
  284. package/dist/components/features/conversation-panel/conversation-panel.cjs +1 -1
  285. package/dist/components/features/conversation-panel/conversation-panel.js +10 -10
  286. package/dist/components/features/mcp-page/custom-server-editor.cjs +1 -1
  287. package/dist/components/features/mcp-page/custom-server-editor.cjs.map +1 -1
  288. package/dist/components/features/mcp-page/custom-server-editor.js +78 -77
  289. package/dist/components/features/mcp-page/custom-server-editor.js.map +1 -1
  290. package/dist/components/features/mcp-page/install-server-modal.cjs +1 -1
  291. package/dist/components/features/mcp-page/install-server-modal.cjs.map +1 -1
  292. package/dist/components/features/mcp-page/install-server-modal.d.ts +4 -1
  293. package/dist/components/features/mcp-page/install-server-modal.js +135 -136
  294. package/dist/components/features/mcp-page/install-server-modal.js.map +1 -1
  295. package/dist/components/features/mcp-page/installed-server-card.cjs +1 -1
  296. package/dist/components/features/mcp-page/installed-server-card.cjs.map +1 -1
  297. package/dist/components/features/mcp-page/installed-server-card.js +40 -34
  298. package/dist/components/features/mcp-page/installed-server-card.js.map +1 -1
  299. package/dist/components/features/mcp-page/mcp-server-health.cjs +2 -0
  300. package/dist/components/features/mcp-page/mcp-server-health.cjs.map +1 -0
  301. package/dist/components/features/mcp-page/mcp-server-health.d.ts +18 -0
  302. package/dist/components/features/mcp-page/mcp-server-health.js +117 -0
  303. package/dist/components/features/mcp-page/mcp-server-health.js.map +1 -0
  304. package/dist/components/features/plugins/build-plugins-view-model.d.ts +10 -1
  305. package/dist/components/features/plugins/plugin-files-section.d.ts +13 -0
  306. package/dist/components/features/settings/agent-canvas-update-card.cjs +2 -0
  307. package/dist/components/features/settings/agent-canvas-update-card.cjs.map +1 -0
  308. package/dist/components/features/settings/agent-canvas-update-card.d.ts +7 -0
  309. package/dist/components/features/settings/agent-canvas-update-card.js +170 -0
  310. package/dist/components/features/settings/agent-canvas-update-card.js.map +1 -0
  311. package/dist/components/features/settings/integrations-settings-link.cjs +2 -0
  312. package/dist/components/features/settings/integrations-settings-link.cjs.map +1 -0
  313. package/dist/components/features/settings/integrations-settings-link.d.ts +5 -0
  314. package/dist/components/features/settings/integrations-settings-link.js +41 -0
  315. package/dist/components/features/settings/integrations-settings-link.js.map +1 -0
  316. package/dist/components/features/settings/llm-profiles/llm-settings-local-view.cjs +1 -1
  317. package/dist/components/features/settings/llm-profiles/llm-settings-local-view.js +1 -1
  318. package/dist/components/features/settings/settings-desktop-sidebar.cjs +1 -1
  319. package/dist/components/features/settings/settings-desktop-sidebar.cjs.map +1 -1
  320. package/dist/components/features/settings/settings-desktop-sidebar.js +31 -21
  321. package/dist/components/features/settings/settings-desktop-sidebar.js.map +1 -1
  322. package/dist/components/features/settings/settings-mobile-drawer.cjs +1 -1
  323. package/dist/components/features/settings/settings-mobile-drawer.cjs.map +1 -1
  324. package/dist/components/features/settings/settings-mobile-drawer.js +40 -30
  325. package/dist/components/features/settings/settings-mobile-drawer.js.map +1 -1
  326. package/dist/components/features/skills/extensions-navigation.cjs +1 -1
  327. package/dist/components/features/skills/extensions-navigation.cjs.map +1 -1
  328. package/dist/components/features/skills/extensions-navigation.js +54 -33
  329. package/dist/components/features/skills/extensions-navigation.js.map +1 -1
  330. package/dist/config/defaults.cjs +1 -1
  331. package/dist/config/defaults.cjs.map +1 -1
  332. package/dist/config/defaults.js +7 -3
  333. package/dist/config/defaults.js.map +1 -1
  334. package/dist/constants/acp-providers.cjs.map +1 -1
  335. package/dist/constants/acp-providers.js.map +1 -1
  336. package/dist/contexts/conversation-websocket-context.cjs +2 -2
  337. package/dist/contexts/conversation-websocket-context.cjs.map +1 -1
  338. package/dist/contexts/conversation-websocket-context.js +169 -166
  339. package/dist/contexts/conversation-websocket-context.js.map +1 -1
  340. package/dist/hooks/mutation/use-delete-llm-profile.cjs +1 -1
  341. package/dist/hooks/mutation/use-delete-llm-profile.js +1 -0
  342. package/dist/hooks/mutation/use-delete-mcp-server.cjs +1 -1
  343. package/dist/hooks/mutation/use-delete-mcp-server.cjs.map +1 -1
  344. package/dist/hooks/mutation/use-delete-mcp-server.js +8 -6
  345. package/dist/hooks/mutation/use-delete-mcp-server.js.map +1 -1
  346. package/dist/hooks/mutation/use-rename-llm-profile.cjs +1 -1
  347. package/dist/hooks/mutation/use-rename-llm-profile.js +1 -0
  348. package/dist/hooks/mutation/use-save-settings.cjs +1 -1
  349. package/dist/hooks/mutation/use-save-settings.js +3 -3
  350. package/dist/hooks/mutation/use-update-mcp-server.cjs +1 -1
  351. package/dist/hooks/mutation/use-update-mcp-server.cjs.map +1 -1
  352. package/dist/hooks/mutation/use-update-mcp-server.js +11 -9
  353. package/dist/hooks/mutation/use-update-mcp-server.js.map +1 -1
  354. package/dist/hooks/query/query-keys.cjs +1 -1
  355. package/dist/hooks/query/query-keys.cjs.map +1 -1
  356. package/dist/hooks/query/query-keys.d.ts +4 -0
  357. package/dist/hooks/query/query-keys.js +2 -2
  358. package/dist/hooks/query/query-keys.js.map +1 -1
  359. package/dist/hooks/query/use-automation-sdk-version.cjs +2 -0
  360. package/dist/hooks/query/use-automation-sdk-version.cjs.map +1 -0
  361. package/dist/hooks/query/use-automation-sdk-version.d.ts +3 -0
  362. package/dist/hooks/query/use-automation-sdk-version.js +63 -0
  363. package/dist/hooks/query/use-automation-sdk-version.js.map +1 -0
  364. package/dist/hooks/query/use-latest-agent-canvas-version.cjs +2 -0
  365. package/dist/hooks/query/use-latest-agent-canvas-version.cjs.map +1 -0
  366. package/dist/hooks/query/use-latest-agent-canvas-version.d.ts +10 -0
  367. package/dist/hooks/query/use-latest-agent-canvas-version.js +19 -0
  368. package/dist/hooks/query/use-latest-agent-canvas-version.js.map +1 -0
  369. package/dist/hooks/query/use-plugin-file-content.d.ts +7 -0
  370. package/dist/hooks/query/use-resolved-workspaces.cjs +1 -1
  371. package/dist/hooks/query/use-resolved-workspaces.cjs.map +1 -1
  372. package/dist/hooks/query/use-resolved-workspaces.js +25 -26
  373. package/dist/hooks/query/use-resolved-workspaces.js.map +1 -1
  374. package/dist/hooks/query/use-search-subdirs.cjs +1 -1
  375. package/dist/hooks/query/use-search-subdirs.cjs.map +1 -1
  376. package/dist/hooks/query/use-search-subdirs.d.ts +5 -0
  377. package/dist/hooks/query/use-search-subdirs.js +16 -4
  378. package/dist/hooks/query/use-search-subdirs.js.map +1 -1
  379. package/dist/hooks/query/use-task-polling.cjs +1 -1
  380. package/dist/hooks/query/use-task-polling.cjs.map +1 -1
  381. package/dist/hooks/query/use-task-polling.d.ts +19 -3
  382. package/dist/hooks/query/use-task-polling.js +36 -35
  383. package/dist/hooks/query/use-task-polling.js.map +1 -1
  384. package/dist/hooks/use-device-flow.cjs +1 -1
  385. package/dist/hooks/use-device-flow.js +6 -6
  386. package/dist/hooks/use-llm-configured.cjs +1 -1
  387. package/dist/hooks/use-llm-configured.js +3 -3
  388. package/dist/hooks/use-mcp-server-health.cjs +2 -0
  389. package/dist/hooks/use-mcp-server-health.cjs.map +1 -0
  390. package/dist/hooks/use-mcp-server-health.d.ts +11 -0
  391. package/dist/hooks/use-mcp-server-health.js +18 -0
  392. package/dist/hooks/use-mcp-server-health.js.map +1 -0
  393. package/dist/hooks/use-skill-installs.cjs +2 -0
  394. package/dist/hooks/use-skill-installs.cjs.map +1 -0
  395. package/dist/hooks/use-skill-installs.d.ts +10 -0
  396. package/dist/hooks/use-skill-installs.js +21 -0
  397. package/dist/hooks/use-skill-installs.js.map +1 -0
  398. package/dist/hooks/use-tracking.cjs +1 -1
  399. package/dist/hooks/use-tracking.cjs.map +1 -1
  400. package/dist/hooks/use-tracking.d.ts +3 -3
  401. package/dist/hooks/use-tracking.js +67 -52
  402. package/dist/hooks/use-tracking.js.map +1 -1
  403. package/dist/i18n/declaration.cjs +1 -1
  404. package/dist/i18n/declaration.cjs.map +1 -1
  405. package/dist/i18n/declaration.d.ts +35 -1
  406. package/dist/i18n/declaration.js +1 -1
  407. package/dist/i18n/declaration.js.map +1 -1
  408. package/dist/i18n/translation.cjs +3 -3
  409. package/dist/i18n/translation.cjs.map +1 -1
  410. package/dist/i18n/translation.js +623 -45
  411. package/dist/i18n/translation.js.map +1 -1
  412. package/dist/index.cjs +1 -1
  413. package/dist/index.js +1 -2
  414. package/dist/lib/index.cjs +1 -1
  415. package/dist/lib/index.d.ts +0 -1
  416. package/dist/lib/index.js +0 -1
  417. package/dist/locales/ar/openhands.json +38 -4
  418. package/dist/locales/ca/openhands.json +38 -4
  419. package/dist/locales/de/openhands.json +38 -4
  420. package/dist/locales/en/openhands.json +38 -4
  421. package/dist/locales/es/openhands.json +38 -4
  422. package/dist/locales/fr/openhands.json +38 -4
  423. package/dist/locales/it/openhands.json +38 -4
  424. package/dist/locales/ja/openhands.json +38 -4
  425. package/dist/locales/ko-KR/openhands.json +38 -4
  426. package/dist/locales/no/openhands.json +38 -4
  427. package/dist/locales/pt/openhands.json +38 -4
  428. package/dist/locales/tr/openhands.json +38 -4
  429. package/dist/locales/uk/openhands.json +38 -4
  430. package/dist/locales/zh-CN/openhands.json +38 -4
  431. package/dist/locales/zh-TW/openhands.json +38 -4
  432. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian-rovo.cjs +2 -0
  433. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian-rovo.cjs.map +1 -0
  434. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian-rovo.js +44 -0
  435. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian-rovo.js.map +1 -0
  436. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.cjs +1 -1
  437. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.cjs.map +1 -1
  438. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.js +13 -0
  439. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.js.map +1 -1
  440. package/dist/node_modules/@openhands/extensions/integrations/catalog/github.cjs +1 -1
  441. package/dist/node_modules/@openhands/extensions/integrations/catalog/github.cjs.map +1 -1
  442. package/dist/node_modules/@openhands/extensions/integrations/catalog/github.js +21 -1
  443. package/dist/node_modules/@openhands/extensions/integrations/catalog/github.js.map +1 -1
  444. package/dist/node_modules/@openhands/extensions/integrations/catalog/linear.cjs +1 -1
  445. package/dist/node_modules/@openhands/extensions/integrations/catalog/linear.cjs.map +1 -1
  446. package/dist/node_modules/@openhands/extensions/integrations/catalog/linear.js +1 -1
  447. package/dist/node_modules/@openhands/extensions/integrations/catalog/linear.js.map +1 -1
  448. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-outlook.cjs +1 -1
  449. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-outlook.cjs.map +1 -1
  450. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-outlook.js +13 -0
  451. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-outlook.js.map +1 -1
  452. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-teams.cjs +1 -1
  453. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-teams.cjs.map +1 -1
  454. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-teams.js +13 -0
  455. package/dist/node_modules/@openhands/extensions/integrations/catalog/microsoft-teams.js.map +1 -1
  456. package/dist/node_modules/@openhands/extensions/integrations/catalog/notion.cjs +1 -1
  457. package/dist/node_modules/@openhands/extensions/integrations/catalog/notion.cjs.map +1 -1
  458. package/dist/node_modules/@openhands/extensions/integrations/catalog/notion.js +13 -0
  459. package/dist/node_modules/@openhands/extensions/integrations/catalog/notion.js.map +1 -1
  460. package/dist/node_modules/@openhands/extensions/integrations/catalog/onedrive.cjs +1 -1
  461. package/dist/node_modules/@openhands/extensions/integrations/catalog/onedrive.cjs.map +1 -1
  462. package/dist/node_modules/@openhands/extensions/integrations/catalog/onedrive.js +13 -0
  463. package/dist/node_modules/@openhands/extensions/integrations/catalog/onedrive.js.map +1 -1
  464. package/dist/node_modules/@openhands/extensions/integrations/catalog/sharepoint.cjs +1 -1
  465. package/dist/node_modules/@openhands/extensions/integrations/catalog/sharepoint.cjs.map +1 -1
  466. package/dist/node_modules/@openhands/extensions/integrations/catalog/sharepoint.js +13 -0
  467. package/dist/node_modules/@openhands/extensions/integrations/catalog/sharepoint.js.map +1 -1
  468. package/dist/node_modules/@openhands/extensions/integrations/catalog/slack.cjs +1 -1
  469. package/dist/node_modules/@openhands/extensions/integrations/catalog/slack.cjs.map +1 -1
  470. package/dist/node_modules/@openhands/extensions/integrations/catalog/slack.js +16 -2
  471. package/dist/node_modules/@openhands/extensions/integrations/catalog/slack.js.map +1 -1
  472. package/dist/node_modules/@openhands/extensions/integrations/catalog/vanta.cjs +2 -0
  473. package/dist/node_modules/@openhands/extensions/integrations/catalog/vanta.cjs.map +1 -0
  474. package/dist/node_modules/@openhands/extensions/integrations/catalog/vanta.js +46 -0
  475. package/dist/node_modules/@openhands/extensions/integrations/catalog/vanta.js.map +1 -0
  476. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs +1 -1
  477. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs.map +1 -1
  478. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js +74 -70
  479. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js.map +1 -1
  480. package/dist/node_modules/@openhands/extensions/skills/index.cjs +123 -3
  481. package/dist/node_modules/@openhands/extensions/skills/index.cjs.map +1 -1
  482. package/dist/node_modules/@openhands/extensions/skills/index.js +7 -1
  483. package/dist/node_modules/@openhands/extensions/skills/index.js.map +1 -1
  484. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-up.cjs +2 -0
  485. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-up.cjs.map +1 -0
  486. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-up.js +9 -0
  487. package/dist/node_modules/lucide-react/dist/esm/icons/chevron-up.js.map +1 -0
  488. package/dist/node_modules/lucide-react/dist/esm/icons/info.cjs +2 -0
  489. package/dist/node_modules/lucide-react/dist/esm/icons/info.cjs.map +1 -0
  490. package/dist/node_modules/lucide-react/dist/esm/icons/info.js +21 -0
  491. package/dist/node_modules/lucide-react/dist/esm/icons/info.js.map +1 -0
  492. package/dist/package.cjs +1 -1
  493. package/dist/package.cjs.map +1 -1
  494. package/dist/package.js +10 -3
  495. package/dist/package.js.map +1 -1
  496. package/dist/routes/app-settings.cjs +1 -1
  497. package/dist/routes/app-settings.cjs.map +1 -1
  498. package/dist/routes/app-settings.js +113 -67
  499. package/dist/routes/app-settings.js.map +1 -1
  500. package/dist/routes/conversation.cjs +1 -1
  501. package/dist/routes/conversation.cjs.map +1 -1
  502. package/dist/routes/conversation.js +1 -1
  503. package/dist/routes/conversation.js.map +1 -1
  504. package/dist/routes/mcp.cjs +1 -1
  505. package/dist/routes/mcp.cjs.map +1 -1
  506. package/dist/routes/mcp.js +1 -0
  507. package/dist/routes/mcp.js.map +1 -1
  508. package/dist/services/cloud-funnel-analytics.cjs +1 -1
  509. package/dist/services/cloud-funnel-analytics.cjs.map +1 -1
  510. package/dist/services/cloud-funnel-analytics.d.ts +2 -2
  511. package/dist/services/cloud-funnel-analytics.js +18 -16
  512. package/dist/services/cloud-funnel-analytics.js.map +1 -1
  513. package/dist/services/settings.cjs +1 -1
  514. package/dist/services/settings.cjs.map +1 -1
  515. package/dist/services/settings.js +1 -0
  516. package/dist/services/settings.js.map +1 -1
  517. package/dist/services/telemetry-context.cjs +2 -0
  518. package/dist/services/telemetry-context.cjs.map +1 -0
  519. package/dist/services/telemetry-context.d.ts +17 -0
  520. package/dist/services/telemetry-context.js +27 -0
  521. package/dist/services/telemetry-context.js.map +1 -0
  522. package/dist/services/telemetry.cjs +1 -1
  523. package/dist/services/telemetry.cjs.map +1 -1
  524. package/dist/services/telemetry.d.ts +7 -5
  525. package/dist/services/telemetry.js +132 -123
  526. package/dist/services/telemetry.js.map +1 -1
  527. package/dist/stores/skill-install-banner-store.cjs +2 -0
  528. package/dist/stores/skill-install-banner-store.cjs.map +1 -0
  529. package/dist/stores/skill-install-banner-store.d.ts +32 -0
  530. package/dist/stores/skill-install-banner-store.js +14 -0
  531. package/dist/stores/skill-install-banner-store.js.map +1 -0
  532. package/dist/stores/use-event-store.cjs +1 -1
  533. package/dist/stores/use-event-store.cjs.map +1 -1
  534. package/dist/stores/use-event-store.js +35 -35
  535. package/dist/stores/use-event-store.js.map +1 -1
  536. package/dist/types/mcp-health.cjs +2 -0
  537. package/dist/types/mcp-health.cjs.map +1 -0
  538. package/dist/types/mcp-health.d.ts +21 -0
  539. package/dist/types/mcp-health.js +6 -0
  540. package/dist/types/mcp-health.js.map +1 -0
  541. package/dist/types/settings.d.ts +1 -0
  542. package/dist/utils/handle-event-for-ui.cjs +1 -1
  543. package/dist/utils/handle-event-for-ui.cjs.map +1 -1
  544. package/dist/utils/handle-event-for-ui.d.ts +5 -0
  545. package/dist/utils/handle-event-for-ui.js +71 -57
  546. package/dist/utils/handle-event-for-ui.js.map +1 -1
  547. package/dist/utils/mcp-credential-validation.cjs +1 -1
  548. package/dist/utils/mcp-credential-validation.cjs.map +1 -1
  549. package/dist/utils/mcp-credential-validation.js +32 -16
  550. package/dist/utils/mcp-credential-validation.js.map +1 -1
  551. package/dist/utils/mcp-server-health-key.cjs +2 -0
  552. package/dist/utils/mcp-server-health-key.cjs.map +1 -0
  553. package/dist/utils/mcp-server-health-key.d.ts +16 -0
  554. package/dist/utils/mcp-server-health-key.js +23 -0
  555. package/dist/utils/mcp-server-health-key.js.map +1 -0
  556. package/dist/utils/mcp-test-error-message.cjs +2 -0
  557. package/dist/utils/mcp-test-error-message.cjs.map +1 -0
  558. package/dist/utils/mcp-test-error-message.d.ts +8 -0
  559. package/dist/utils/mcp-test-error-message.js +14 -0
  560. package/dist/utils/mcp-test-error-message.js.map +1 -0
  561. package/dist/utils/redact-mcp-secrets.cjs +2 -0
  562. package/dist/utils/redact-mcp-secrets.cjs.map +1 -0
  563. package/dist/utils/redact-mcp-secrets.d.ts +13 -0
  564. package/dist/utils/redact-mcp-secrets.js +57 -0
  565. package/dist/utils/redact-mcp-secrets.js.map +1 -0
  566. package/dist/utils/settings-utils.cjs.map +1 -1
  567. package/dist/utils/settings-utils.js.map +1 -1
  568. package/dist/utils/skill-install-events.cjs +3 -0
  569. package/dist/utils/skill-install-events.cjs.map +1 -0
  570. package/dist/utils/skill-install-events.d.ts +16 -0
  571. package/dist/utils/skill-install-events.js +26 -0
  572. package/dist/utils/skill-install-events.js.map +1 -0
  573. package/dist/utils/title-llm-profile.cjs +2 -0
  574. package/dist/utils/title-llm-profile.cjs.map +1 -0
  575. package/dist/utils/title-llm-profile.d.ts +2 -0
  576. package/dist/utils/title-llm-profile.js +11 -0
  577. package/dist/utils/title-llm-profile.js.map +1 -0
  578. package/package.json +10 -3
  579. package/scripts/check-sdk-version-sync.mjs +6 -6
  580. package/scripts/check-translation-completeness.cjs +2 -0
  581. package/scripts/dev-process-utils.mjs +62 -1
  582. package/scripts/dev-safe.mjs +2 -2
  583. package/scripts/dev-static.mjs +5 -3
  584. package/scripts/dev-with-automation.mjs +98 -14
  585. package/scripts/download-node.mjs +382 -0
  586. package/scripts/download-uv.mjs +204 -0
  587. package/scripts/logger.mjs +64 -35
  588. package/build/assets/agent-server-compatibility-BWN-EIQn.js +0 -1
  589. package/build/assets/agent-server-conversation-service.api-DSPIkMwJ.js +0 -41
  590. package/build/assets/agent-settings-CgCnqaAA.js +0 -2
  591. package/build/assets/agent-settings-DDkSavD7.js +0 -1
  592. package/build/assets/api-key-entry-screen-iZc7n8AI.js +0 -1
  593. package/build/assets/app-settings-B5zyipxJ.js +0 -1
  594. package/build/assets/automation-export-BwRX6H32.js +0 -2
  595. package/build/assets/automations-list-lHAVzMoA.js +0 -1
  596. package/build/assets/backend-form-modal-RGcu909r.js +0 -1
  597. package/build/assets/browser-store-BI6uklCG.js +0 -1
  598. package/build/assets/chat-send-button-BZHG2hAW.js +0 -1
  599. package/build/assets/client-source-BZK8vRs2.js +0 -1
  600. package/build/assets/cloud-funnel-analytics-Zcv57hfT.js +0 -1
  601. package/build/assets/conversation-BoCYx8n2.js +0 -89
  602. package/build/assets/conversation-DgHqMjnY.js +0 -1
  603. package/build/assets/conversation-panel-78Rxh_-I.js +0 -1
  604. package/build/assets/conversation-state-store-B2Bp32XI.js +0 -1
  605. package/build/assets/declaration-BGsHjPEW.js +0 -1
  606. package/build/assets/extensions-hub-CCbLA7IM.js +0 -1
  607. package/build/assets/extensions-navigation-CPNjMNvR.js +0 -1
  608. package/build/assets/files-tab-store-DJsPxYdg.js +0 -1
  609. package/build/assets/files-tab-y4K7EXBO.js +0 -1
  610. package/build/assets/git-provider-icon-ITvsH4Ko.js +0 -1
  611. package/build/assets/home-BwbeOWwU.js +0 -1
  612. package/build/assets/home-DKPGQ5P3.js +0 -1
  613. package/build/assets/index-home-Bko2Itv5.js +0 -1
  614. package/build/assets/install-server-modal-Qk1gwyI8.js +0 -1
  615. package/build/assets/lesson-plan-DMCPdtrf.js +0 -4
  616. package/build/assets/llm-not-configured-banner-C1ktyb3C.js +0 -27
  617. package/build/assets/llm-settings-BBsZFi9l.js +0 -1
  618. package/build/assets/llm-settings-DFkJQr_w.js +0 -1
  619. package/build/assets/manage-backends-modal-CKQ46rvw.js +0 -1
  620. package/build/assets/manifest-03478bbf.js +0 -1
  621. package/build/assets/markdown-renderer-B4V2OMFP.js +0 -2
  622. package/build/assets/mcp-BapoJsDc.js +0 -9
  623. package/build/assets/messages-CUBFMY5o.js +0 -40
  624. package/build/assets/objectWithoutPropertiesLoose-BgYIAs2y.js +0 -1
  625. package/build/assets/onboarding-6NMkdNxV.js +0 -1
  626. package/build/assets/onboarding-modal-Q4AnpIdg.js +0 -1
  627. package/build/assets/recommended-automations-launcher-Bj7PuHY1.js +0 -58
  628. package/build/assets/refresh-cw-B9B7-vsy.js +0 -1
  629. package/build/assets/root-CDnuTToz.js +0 -2
  630. package/build/assets/root-CYWeTrWE.css +0 -1
  631. package/build/assets/root-layout-DfYVlcRG.js +0 -2
  632. package/build/assets/sdk-section-page-DLLNrYjI.js +0 -1
  633. package/build/assets/secrets-settings-CHy72A5C.js +0 -1
  634. package/build/assets/settings-B9S8UVQr.js +0 -1
  635. package/build/assets/settings-index-BKDl2CQ-.js +0 -1
  636. package/build/assets/settings-modal-DeK11CyT.js +0 -1
  637. package/build/assets/shared-conversation-9atDzTR4.js +0 -1
  638. package/build/assets/sidebar-mobile-menu-toggle-giMMZctn.js +0 -1
  639. package/build/assets/skills-plugins-Tropusio.js +0 -1
  640. package/build/assets/skills-settings-BvzBBCWk.js +0 -2
  641. package/build/assets/telemetry-C5JMEfqB.js +0 -2
  642. package/build/assets/use-activate-agent-profile-DlykqEfN.js +0 -1
  643. package/build/assets/use-backends-health-CLHPitJM.js +0 -1
  644. package/build/assets/use-config-B5r8erYt.js +0 -1
  645. package/build/assets/use-event-store-Bbaz51GR.js +0 -4
  646. package/build/assets/use-llm-profiles-Dr5__SlZ.js +0 -1
  647. package/build/assets/use-plugins-marketplace-BplU65ae.js +0 -1
  648. package/build/assets/use-runtime-is-ready-vCFOcLFa.js +0 -1
  649. package/build/assets/use-settings-CyxSlyTQ.js +0 -1
  650. package/build/assets/use-settings-nav-items-zEkSO_Bf.js +0 -16
  651. package/build/assets/use-tracking-D2Qg5Lpd.js +0 -1
  652. package/build/assets/useMutationState-OnAsCLA_.js +0 -1
  653. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-BD7v71DM.js +0 -2
  654. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-BT05Osu4.js +0 -28
  655. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-COhHANvV.js +0 -2
  656. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-DgzWSMRc.js +0 -2
  657. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-DqZatR3B.js +0 -7
  658. package/build/assets/vendor~index-home~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-FghQ3Z1a.js +0 -1
  659. package/build/assets/vendor~root-layout~index-home~home~conversation-panel~conversation-BTvhGHHG.js +0 -1
  660. package/build/assets/vendor~root-layout~index-home~home~mcp~automations-list~onboarding-modal-C4-GDgiF.js +0 -1
  661. package/dist/components/features/analytics/telemetry-consent-banner.cjs +0 -2
  662. package/dist/components/features/analytics/telemetry-consent-banner.cjs.map +0 -1
  663. package/dist/components/features/analytics/telemetry-consent-banner.d.ts +0 -29
  664. package/dist/components/features/analytics/telemetry-consent-banner.js +0 -59
  665. package/dist/components/features/analytics/telemetry-consent-banner.js.map +0 -1
  666. /package/build/assets/{__vite-browser-external-BQUj0sYu.js → __vite-browser-external-CKHVBTNC.js} +0 -0
  667. /package/build/assets/{agent-server-ui-style-scope-qhnhZHBV.js → agent-server-ui-style-scope-BvSa9on7.js} +0 -0
  668. /package/build/assets/{automation-LZB0MjdV.js → automation-BGiPmxFR.js} +0 -0
  669. /package/build/assets/{canvas-ui-COgjltGz.js → canvas-ui-CC4EJo-H.js} +0 -0
  670. /package/build/assets/{checkmark-h7udgXMM.js → checkmark-ygQB1AtV.js} +0 -0
  671. /package/build/assets/{close-DLFb5kfv.js → close-BjSA0HWT.js} +0 -0
  672. /package/build/assets/{code-tag-DYlAhEQY.js → code-tag-D9aPPbOe.js} +0 -0
  673. /package/build/assets/{common-CxbUL3nR.js → common-CCuqQy_S.js} +0 -0
  674. /package/build/assets/{conversation-local-storage-F6zVMM--.js → conversation-local-storage-BDjWPg0Z.js} +0 -0
  675. /package/build/assets/{copy-BLmGeJcd.js → copy-Dce7sOvS.js} +0 -0
  676. /package/build/assets/{copy-D1YGjP-C.js → copy-iPJAncAj.js} +0 -0
  677. /package/build/assets/{dist-CodhvuAP.js → dist-D9PhjOaq.js} +0 -0
  678. /package/build/assets/{environment-switch-store-DkjlKWzN.js → environment-switch-store-DHfcz4M4.js} +0 -0
  679. /package/build/assets/{extension-module-card-classes-9V3J9J_b.js → extension-module-card-classes-BpebKiN6.js} +0 -0
  680. /package/build/assets/{external-link-lhdITPqq.js → external-link-CpPMXxrV.js} +0 -0
  681. /package/build/assets/{folder-Bk311brm.js → folder-BabbYnbF.js} +0 -0
  682. /package/build/assets/{format-time-delta-DhDbkD7V.js → format-time-delta-BkAmJEPT.js} +0 -0
  683. /package/build/assets/{get-language-from-path-DBqBsCWH.js → get-language-from-path-CPNnuVhI.js} +0 -0
  684. /package/build/assets/{git-status-mapper-DnL9OC8_.js → git-status-mapper-BIXK0AAG.js} +0 -0
  685. /package/build/assets/{iconBase-Dz51Zn5x.js → iconBase-Ch-fOe8B.js} +0 -0
  686. /package/build/assets/{link-external-CaBmUEs_.js → link-external-DfKQroKX.js} +0 -0
  687. /package/build/assets/{list-todo-CAtvkzcy.js → list-todo-6J6zc8oS.js} +0 -0
  688. /package/build/assets/{llm-subscription-DtsX5Lhg.js → llm-subscription-Bvu8BLXc.js} +0 -0
  689. /package/build/assets/{loader-circle-CsAsfLIs.js → loader-circle-DGsqG5qw.js} +0 -0
  690. /package/build/assets/{map-provider-BbIb12oh.js → map-provider-BSvXvVR0.js} +0 -0
  691. /package/build/assets/{middleware-DYHYNAnP.js → middleware-_izt7V6U.js} +0 -0
  692. /package/build/assets/{modal-backdrop-zVwfOJZr.js → modal-backdrop-B7H42xHG.js} +0 -0
  693. /package/build/assets/{navigation-context-DPs2G0X8.js → navigation-context-cvyvI1WA.js} +0 -0
  694. /package/build/assets/{openhands-logo-bcNw5bkY.js → openhands-logo-pEw0nKFP.js} +0 -0
  695. /package/build/assets/{pencil-CWhApFIT.js → pencil-DmSqWUA3.js} +0 -0
  696. /package/build/assets/{react-DPDgL6du.js → react-MKQOkha9.js} +0 -0
  697. /package/build/assets/{search-BgOCR7_c.js → search-B5D5EuSn.js} +0 -0
  698. /package/build/assets/{search-dIQ09UY2.js → search-vnYL5jwV.js} +0 -0
  699. /package/build/assets/{settings-gear-MXCyZW0x.js → settings-gear-BOwF59Bs.js} +0 -0
  700. /package/build/assets/{settings-section-header-context-COMOIaxV.js → settings-section-header-context-CGgHZXrf.js} +0 -0
  701. /package/build/assets/{skills-EqzxYBvT.js → skills-arUOHLcS.js} +0 -0
  702. /package/build/assets/{square-chevron-right-DE-JN-2Z.js → square-chevron-right-EizCGtgq.js} +0 -0
  703. /package/build/assets/{switch-skeleton-DqQwalyu.js → switch-skeleton-BjPRx1QD.js} +0 -0
  704. /package/build/assets/{terminal-Cgf___rK.js → terminal-a57bHgZs.js} +0 -0
  705. /package/build/assets/{trash-2-BU6BlW4i.js → trash-2-CF3hilrY.js} +0 -0
  706. /package/build/assets/{u-check-circle-BqoIxNz_.js → u-check-circle-WGqHdtfO.js} +0 -0
  707. /package/build/assets/{u-check-circle-half-MsShT8Ec.js → u-check-circle-half-LiwZkuFe.js} +0 -0
  708. /package/build/assets/{u-edit-DL5LgKon.js → u-edit-BwHepAPi.js} +0 -0
  709. /package/build/assets/{use-breakpoint-BP3lqLMx.js → use-breakpoint-B-O0lQ84.js} +0 -0
  710. /package/build/assets/{use-click-outside-element-D9BYdANT.js → use-click-outside-element-DLQIMQrz.js} +0 -0
  711. /package/build/assets/{use-is-authed-BezEyfeh.js → use-is-authed-BSeo4coy.js} +0 -0
  712. /package/build/assets/{use-onboarding-completion-C_Swglzp.js → use-onboarding-completion-C0hPNGpw.js} +0 -0
  713. /package/build/assets/{v4-DbCkTSoU.js → v4-q4yWTlMz.js} +0 -0
  714. /package/build/assets/{vendor~browser-By2MVv_N.js → vendor~browser-UwB1jbAZ.js} +0 -0
  715. /package/build/assets/{vendor~conversation-panel~conversation~alert-banner-CxfzCmkN.js → vendor~conversation-panel~conversation~alert-banner-DHDkknt5.js} +0 -0
  716. /package/build/assets/{vendor~index-home~home~conversation-panel~conversation-ChmYLVgz.js → vendor~index-home~home~conversation-panel~conversation-Bff9cq5J.js} +0 -0
  717. /package/build/assets/{vendor~module-D5a_AClf.js → vendor~module-D4DeFV4J.js} +0 -0
  718. /package/build/assets/{vendor~terminal-CnKZILnC.js → vendor~terminal-B7Eiak0-.js} +0 -0
  719. /package/build/assets/{x-ZE4iplpF.js → x-DQB_00VJ.js} +0 -0
  720. /package/build/assets/{x-mark-Batp6gXa.js → x-mark-B32XuEwS.js} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"acp-providers.js","names":[],"sources":["../../src/constants/acp-providers.ts"],"sourcesContent":["import { getAcpProvider as getClientAcpProvider } from \"@openhands/typescript-client\";\nimport { I18nKey } from \"#/i18n/declaration\";\n\n/** Upstream registry fields not yet on the pinned client's exported type. */\ntype ClientAcpProviderRegistry = NonNullable<\n ReturnType<typeof getClientAcpProvider>\n> & {\n available_models?: Array<{ id: string; label: string }>;\n default_model?: string;\n};\n\nexport type ACPProviderIcon =\n | \"claude-code\"\n | \"codex\"\n | \"gemini\"\n | \"cli-generic\";\n\nexport const ACP_PROVIDER_FALLBACK_ICON: ACPProviderIcon = \"cli-generic\";\n\n// Sentinel ``agent.llm.model`` returned by older SDKs for ACP conversations\n// in lieu of a real model. Suppressed at every consumer that resolves a\n// display string.\n//\n// NB: ``\"default\"`` is NOT a placeholder. claude-agent-acp 0.44+ exposes\n// ``default`` (\"Default (recommended)\") as a real, selectable model in its\n// configOptions select — the server reports it as ``currentModelId`` and lists\n// it in ``availableModels`` — so it must surface like any other model id.\nexport const ACP_MANAGED_SENTINEL = \"acp-managed\";\n\n/**\n * Filter for \"real\" ACP model strings — non-empty and not the legacy\n * ``acp-managed`` sentinel. Returns the trimmed value on success, ``null``\n * otherwise.\n */\nfunction realAcpModel(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n if (trimmed === ACP_MANAGED_SENTINEL) return null;\n return trimmed;\n}\n\n/**\n * Single source of truth for resolving the model string to surface for an\n * ACP conversation/settings context. Consumed by the conversation adapter\n * (chip text), the conversation-creation path (concrete ``acp_model``\n * payload), the Settings → Agent form (initial value), and the chat-input\n * model label.\n *\n * Precedence: SDK runtime fields → user-configured ``acp_model`` →\n * legacy ``agent.llm.model`` → provider default (when ``providerDefault``\n * is passed). Pass ``providerDefault`` only on surfaces that should\n * silently substitute the registry default; omit it for the conversation\n * chip, which must distinguish \"no concrete model\" from \"default\".\n */\nexport function resolveEffectiveAcpModel(inputs: {\n runtimeName?: string | null;\n runtimeId?: string | null;\n configured?: string | null;\n sdkLlm?: string | null;\n providerDefault?: string | null;\n}): string | null {\n for (const candidate of [\n inputs.runtimeName,\n inputs.runtimeId,\n inputs.configured,\n inputs.sdkLlm,\n ]) {\n const value = realAcpModel(candidate);\n if (value) return value;\n }\n return inputs.providerDefault ?? null;\n}\n\n/**\n * Shape of a built-in ACP (Agent Client Protocol) provider as Canvas consumes\n * it. The data fields (display name, launch command, model picker + default)\n * are sourced at module load from ``@openhands/typescript-client``'s ACP\n * registry — the generated mirror of the Python source of truth\n * ``openhands.sdk.settings.acp_providers``. This config only adds the\n * Canvas-specific UI fields ({@link ACPProviderConfig.icon} +\n * {@link ACPProviderConfig.description_key}); see {@link ACP_PROVIDER_UI}.\n */\nexport interface ACPProviderConfig {\n /** Stable registry key, also stored on conversations as ``tags.acpserver``. */\n key: string;\n /** Human-readable name shown in dropdowns and conversation chips. */\n display_name: string;\n /**\n * Tokens passed to the agent-server as ``acp_command`` when this preset\n * is picked. Each entry must be a real ACP-protocol stdio server — the\n * SDK validates this against the {@link ACPProviderConfig.key}.\n *\n * NB: ``npx -y @openai/codex acp`` looks plausible but is **not** an\n * ACP server — the codex CLI has no ``acp`` subcommand and exits with\n * ``Error: stdin is not a terminal`` when spawned without a TTY, which\n * silently deadlocks the agent-server's ACP handshake. Use\n * ``@agentclientprotocol/codex-acp`` (the codex ACP wrapper) instead.\n */\n default_command: string[];\n /**\n * Suggested ACP model IDs for the provider's picker, sourced from the\n * typescript-client registry. Not authoritative access checks; users can\n * still enter a custom override in Settings -> Agent.\n */\n available_models?: ACPModelOption[];\n /** Model ID preselected for built-in providers so Canvas never saves blank. */\n default_model?: string;\n /**\n * i18n key for the one-line provider description rendered under the\n * onboarding tile. Stored on the registry so adding a new ACP\n * provider only requires editing this file (not the onboarding tile\n * list separately).\n */\n description_key: I18nKey;\n /**\n * Serializable icon key used by UI surfaces that render provider\n * choices. Kept as a string so the SDK mirror check can continue to\n * parse this registry without importing React components.\n */\n icon?: ACPProviderIcon;\n}\n\nexport interface ACPModelOption {\n /** Exact model ID sent as ``acp_model``. */\n id: string;\n /** Human-readable label shown in Settings -> Agent. */\n label: string;\n}\n\n// Canvas-only UI metadata per built-in provider, keyed by the ACP registry\n// key. Everything else — display name, launch command, model picker list and\n// default — comes from the typescript-client registry below. Adding a model\n// or a provider happens upstream in the SDK; Canvas only owns the brand icon\n// and the onboarding-tile description here. A provider with no entry here is\n// intentionally not surfaced in the UI.\nconst ACP_PROVIDER_UI: Record<\n string,\n { icon: ACPProviderIcon; description_key: I18nKey }\n> = {\n \"claude-code\": {\n icon: \"claude-code\",\n description_key: I18nKey.ONBOARDING$AGENT_CLAUDE_CODE_DESCRIPTION,\n },\n codex: {\n icon: \"codex\",\n description_key: I18nKey.ONBOARDING$AGENT_CODEX_DESCRIPTION,\n },\n \"gemini-cli\": {\n icon: \"gemini\",\n description_key: I18nKey.ONBOARDING$AGENT_GEMINI_CLI_DESCRIPTION,\n },\n};\n\n// Built-in ACP providers Canvas surfaces, built by enriching each upstream\n// registry record (``@openhands/typescript-client`` → Python SDK) with the\n// Canvas UI metadata above. Model lists + defaults are no longer hand-kept\n// here (closes agent-canvas#740) — they track the SDK via the pinned client.\nexport const ACP_PROVIDERS: ACPProviderConfig[] = Object.entries(\n ACP_PROVIDER_UI,\n).map(([key, ui]) => {\n const info = getClientAcpProvider(key) as ClientAcpProviderRegistry | null;\n return {\n key,\n display_name: info?.display_name ?? key,\n default_command: info ? [...info.default_command] : [],\n available_models: info?.available_models?.map((model) => ({\n id: model.id,\n label: model.label,\n })),\n default_model: info?.default_model ?? undefined,\n description_key: ui.description_key,\n icon: ui.icon,\n };\n});\n\nexport const ACP_CUSTOM_PRESET_KEY = \"custom\";\n\n/**\n * A credential an ACP provider authenticates with, surfaced during onboarding\n * so the user can populate it without leaving the flow. The {@link name} is\n * both the global-secret name and the environment variable the agent-server\n * exports into the ACP subprocess — keeping them identical is what makes a\n * saved secret actually reach the provider CLI.\n */\nexport interface ACPProviderSecretField {\n /** Secret name and env var (e.g. ``\"ANTHROPIC_API_KEY\"``). Must satisfy the\n * secret-name pattern ``^[a-zA-Z][a-zA-Z0-9_]{0,63}$``. */\n name: string;\n /** Render as a masked password input (API keys, OAuth tokens, credential\n * blobs) rather than a plain-text input (base URLs, project IDs). Doubles as\n * \"this is an actual credential\": when the onboarding step is required, only\n * a ``secret`` field satisfies it — a base URL or GCP scalar alone can't\n * authenticate anything. */\n secret?: boolean;\n /** Render as a multi-line textarea rather than a single-line input. Set for\n * file-content credentials the user pastes verbatim (Codex ``auth.json``,\n * Gemini Vertex service-account / ADC JSON) — the ones the SDK materialises\n * to disk, which a cloud backend can't consume yet (agent-canvas#1016). */\n multiline?: boolean;\n /** i18n key for the one-line helper text under the field. */\n hint_key: I18nKey;\n /**\n * Interpolation values for {@link hint_key} (e.g. ``{ file: \"~/.codex/auth.json\" }``\n * for the shared \"paste the file contents\" hint). Omitted for hints that\n * take no parameters.\n */\n hint_values?: Record<string, string>;\n}\n\n/**\n * Container credentials — beyond the registry's API key / base URL — that\n * canvas collects per provider so a *containerized* agent-server (no host login\n * state) can authenticate the ACP CLI: subscription tokens plus the file-content\n * blobs and Vertex config the SDK's ``acp_file_secrets`` materialisation\n * consumes (agent-canvas#1013/#1014). They have no registry env-var entry\n * because they're a deployment concern, not a model registry field. The Gemini\n * project/location/flag are plain config, not secrets — grouped here because\n * they travel with the SA blob.\n *\n * The blob *names* (``CODEX_AUTH_JSON`` / ``GOOGLE_APPLICATION_CREDENTIALS_JSON``)\n * duplicate the SDK registry's ``file_secrets`` specs; derive them from the\n * client registry once the pinned ``@openhands/typescript-client`` mirrors that\n * field (same bump that unblocks ``acp_isolate_data_dir``, see #1019).\n */\nconst ACP_RESERVED_CREDENTIALS: Record<string, ACPProviderSecretField[]> = {\n codex: [\n {\n name: \"CODEX_AUTH_JSON\",\n secret: true,\n multiline: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_FILE_BLOB_HINT,\n hint_values: { file: \"~/.codex/auth.json\" },\n },\n ],\n \"claude-code\": [\n {\n name: \"CLAUDE_CODE_OAUTH_TOKEN\",\n secret: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_OAUTH_TOKEN_HINT,\n },\n ],\n \"gemini-cli\": [\n {\n name: \"GOOGLE_APPLICATION_CREDENTIALS_JSON\",\n secret: true,\n multiline: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_FILE_BLOB_HINT,\n hint_values: {\n file: \"~/.config/gcloud/application_default_credentials.json\",\n },\n },\n {\n name: \"GOOGLE_CLOUD_PROJECT\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_GCP_PROJECT_HINT,\n },\n {\n name: \"GOOGLE_CLOUD_LOCATION\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_GCP_LOCATION_HINT,\n },\n {\n name: \"GOOGLE_GENAI_USE_VERTEXAI\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_VERTEXAI_FLAG_HINT,\n },\n ],\n};\n\n/**\n * Credential pairs that break each other at runtime, keyed by provider —\n * mirrors the SDK's ``_ENV_CONFLICT_MAP`` (software-agent-sdk#3588). Claude's\n * OAuth token (``CLAUDE_CODE_OAUTH_TOKEN``) authenticates against Anthropic\n * directly, so when it is set the SDK strips:\n * - ``ANTHROPIC_API_KEY`` — otherwise it takes precedence and silently\n * bypasses the subscription;\n * - ``ANTHROPIC_BASE_URL`` — otherwise it proxies the bearer to an endpoint\n * that rejects it (see docs/ACP_AGENTS.md).\n * Either one set alongside the token is silently ignored at runtime, so warn.\n */\nconst ACP_CREDENTIAL_CONFLICTS: Record<string, Array<[string, string]>> = {\n \"claude-code\": [\n [\"CLAUDE_CODE_OAUTH_TOKEN\", \"ANTHROPIC_API_KEY\"],\n [\"CLAUDE_CODE_OAUTH_TOKEN\", \"ANTHROPIC_BASE_URL\"],\n ],\n};\n\n/**\n * The ``[credential, conflicting]`` pairs the user currently has set for\n * ``providerKey``. ``hasValue`` should cover both typed and already-saved\n * values, since a previously saved secret conflicts just the same.\n */\nexport function getAcpCredentialConflicts(\n key: string | null | undefined,\n hasValue: (name: string) => boolean,\n): Array<[string, string]> {\n if (!key) return [];\n return (ACP_CREDENTIAL_CONFLICTS[key] ?? []).filter(\n ([credential, conflicting]) =>\n hasValue(credential) && hasValue(conflicting),\n );\n}\n\n/**\n * Vertex AI-safe ``acp_model`` for Gemini.\n *\n * Left unset, gemini-cli falls back to its own internal default — a preview\n * flash model that 404s on Google Cloud projects without preview access. And a\n * ``*-flash`` id is no safer to pin: gemini-cli re-resolves flash ids at\n * generation time to its *current default* flash (software-agent-sdk#3532), so\n * a pinned ``gemini-2.5-flash`` still ran ``gemini-3-flash``. Only a non-flash\n * id sticks; ``gemini-2.5-pro`` is broadly available on Vertex and the\n * API-key / Google-login paths, so canvas preselects it. Kept as a named\n * constant so every model-default consumer agrees.\n */\nexport const ACP_VERTEX_SAFE_MODEL = \"gemini-2.5-pro\";\n\n/**\n * The default ``acp_model`` canvas substitutes for ``providerKey`` wherever no\n * concrete model is configured — settings seeding (onboarding, Settings →\n * Agent), the {@link buildAcpAgentSettingsDiff} fallback, and the start-request\n * fallback for a saved ``null``. Overrides only Gemini (→\n * {@link ACP_VERTEX_SAFE_MODEL}, see why above); every other provider keeps its\n * registry {@link ACPProviderConfig.default_model}. Returns ``null`` when\n * there's no override and no registry default, letting the ACP server pick its\n * own.\n *\n * Distinct from {@link ACPProviderConfig.default_model} (which mirrors the SDK\n * registry verbatim, closing agent-canvas#740): this is the *preferred* default,\n * deliberately diverging for Gemini where the registry value isn't safe on\n * every backend — so every default-model surface must route through this, not\n * read ``default_model`` directly.\n */\nexport function getAcpPreferredDefaultModel(\n key: string | null | undefined,\n): string | null {\n if (key === \"gemini-cli\") return ACP_VERTEX_SAFE_MODEL;\n return getAcpProvider(key)?.default_model ?? null;\n}\n\n/**\n * List the credentials Canvas should prompt for when onboarding the given ACP\n * provider. The API-key and base-URL field *names* track the SDK registry's\n * ``api_key_env_var`` / ``base_url_env_var`` (mirrored via\n * ``@openhands/typescript-client``) so they can't drift as providers are added\n * or renamed; the per-provider container credentials (subscription / Vertex\n * blobs) come from {@link ACP_RESERVED_CREDENTIALS}, since those are a\n * containerized-deployment concern with no model-registry entry. Each field\n * name equals the env var the agent-server exports into the provider subprocess\n * (which is what makes a saved secret reach the CLI). On the wire none of this\n * is special — every saved credential travels uniformly as a ``LookupSecret``.\n *\n * Field order is: container subscription/Vertex credentials → API key →\n * optional base URL. Every field is optional at the UI level — the step is\n * skippable, and a subscription / OAuth login on the backend takes precedence\n * over a key at runtime. (Whether the *step* is required is a\n * backend-capability decision the onboarding modal makes; see\n * ``backendRequiresAcpCredentials``.)\n *\n * NB: the base URL is rendered plain-text (not ``secret``), so it never counts\n * toward a required credential step — setting ``ANTHROPIC_BASE_URL`` alongside\n * a ``CLAUDE_CODE_OAUTH_TOKEN`` breaks the token's bearer auth, which the forms\n * surface via {@link getAcpCredentialConflicts}.\n *\n * Returns ``[]`` for OpenHands, the ``\"custom\"`` preset, any unknown key, and a\n * future OAuth-only provider whose registry entry has no ``api_key_env_var`` —\n * callers treat an empty list as \"no credentials step for this provider\".\n */\nexport function getAcpProviderSecrets(\n key: string | null | undefined,\n): ACPProviderSecretField[] {\n if (!key) return [];\n const info = getClientAcpProvider(key);\n if (!info) return [];\n // Subscription / Vertex credentials first — they're the primary auth path for\n // ACP providers (Claude Pro/Max OAuth token, Codex ChatGPT auth.json), with\n // the API key as the fallback below.\n const fields: ACPProviderSecretField[] = [\n ...(ACP_RESERVED_CREDENTIALS[key] ?? []),\n ];\n if (info.api_key_env_var) {\n fields.push({\n name: info.api_key_env_var,\n secret: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_API_KEY_HINT,\n });\n }\n if (info.base_url_env_var) {\n fields.push({\n name: info.base_url_env_var,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_BASE_URL_HINT,\n });\n }\n return fields;\n}\n\n/**\n * Look up a built-in ACP provider config by its registry key.\n *\n * Returns ``undefined`` for an empty / null key, for the ``\"custom\"`` preset\n * (which has no registry entry), and for any forward-compatible key Canvas's\n * registry doesn't know about yet. Centralizes the ``ACP_PROVIDERS.find(...)``\n * lookup shared by the resolvers below and by the adapter / settings surfaces\n * so the key-comparison shape lives in one place.\n */\nexport function getAcpProvider(\n key: string | null | undefined,\n): ACPProviderConfig | undefined {\n if (!key) return undefined;\n return ACP_PROVIDERS.find((provider) => provider.key === key);\n}\n\n/**\n * Resolve an ACP provider registry key (the value stored under\n * ``tags.acpserver`` on a conversation) to a human display name for the\n * sidebar chip.\n *\n * Returns ``null`` for an empty / null key and for keys not in\n * {@link ACP_PROVIDERS} — most notably ``\"custom\"`` (the user-supplied\n * command preset has no canonical brand name) and any forward-compatible\n * value Canvas's registry doesn't know about yet. Callers should fall\n * back to a generic ``\"ACP\"`` label in that case so the chip still\n * communicates \"this is an ACP conversation\".\n *\n * Kept separate from {@link buildAcpAgentSettingsDiff}'s lookup so the\n * conversation-card render path can resolve display names without\n * importing the settings-payload builder.\n */\nexport function getAcpProviderDisplayName(\n key: string | null | undefined,\n): string | null {\n const found = getAcpProvider(key);\n return found ? found.display_name : null;\n}\n\n/**\n * Resolve an ACP provider registry key to the icon discriminator the\n * conversation chip should render alongside the model text.\n *\n * Falls back to {@link ACP_PROVIDER_FALLBACK_ICON} for ``\"custom\"``,\n * unknown keys, or a missing key — the chip then shows a neutral\n * terminal glyph that still communicates \"this is an ACP conversation\"\n * without claiming a brand identity we don't know.\n */\nexport function resolveAcpProviderIcon(\n key: string | null | undefined,\n): ACPProviderIcon {\n return getAcpProvider(key)?.icon ?? ACP_PROVIDER_FALLBACK_ICON;\n}\n\n/**\n * Resolve a raw ``acp_model`` ID to the human-readable label the provider's\n * picker shows for it (e.g. ``\"claude-opus-4-7\"`` → ``\"Claude Opus 4.7\"``).\n *\n * Falls back to the raw ID when the provider is unknown or the ID isn't one\n * of its registered {@link ACPModelOption}s — so a user's custom override\n * still renders something meaningful rather than nothing. Returns ``null``\n * only when there is no model to show, letting the conversation chip decide\n * to display the provider name instead.\n */\nexport function labelForAcpModel(\n serverKey: string | null | undefined,\n modelId: string | null | undefined,\n): string | null {\n if (!modelId) return null;\n const provider = getAcpProvider(serverKey);\n const match = provider?.available_models?.find((m) => m.id === modelId);\n return match?.label ?? modelId;\n}\n\n/**\n * Build the ``agent_settings_diff`` payload PATCH /api/settings expects\n * for the agent-kind/provider choice the user just made.\n *\n * Used by both the Settings → Agent page and the onboarding \"choose\n * agent\" step — keeping the shape in one helper means a future change\n * (e.g. always seeding ``acp_command`` from the registry instead of\n * sending ``[]``, or adding new ``acp_*`` reset fields) lands in both\n * surfaces atomically.\n *\n * Returns ``null`` for an unknown ACP provider key by default — the\n * caller can skip the save (the UI shouldn't surface unknown options,\n * but the defensive path keeps a buggy preset list from corrupting\n * settings).\n *\n * Pass ``allowUnknownServer: true`` to opt into pass-through for keys\n * that aren't in {@link ACP_PROVIDERS} or ``ACP_CUSTOM_PRESET_KEY``.\n * The Settings → Agent page uses this when the user opens settings\n * that already carry an ``acp_server`` value canvas's registry\n * doesn't know about (e.g. set out-of-band via the API for a provider\n * we haven't mirrored yet) and saves without changing the command —\n * otherwise the original key would be silently demoted to ``\"custom\"``.\n */\nexport function buildAcpAgentSettingsDiff(\n providerKey: string,\n options: {\n command?: string[];\n model?: string | null;\n allowUnknownServer?: boolean;\n } = {},\n): Record<string, unknown> | null {\n if (providerKey === \"openhands\") {\n // Switching back to OpenHands. The agent-server's ``Settings.update``\n // applies a fresh ``{'agent_kind': ...}`` base whenever the kind\n // flips, so any ``acp_*`` fields would be discarded before\n // validation. Send the kind alone.\n return { agent_kind: \"openhands\" };\n }\n\n const isCustom = providerKey === ACP_CUSTOM_PRESET_KEY;\n const provider = isCustom ? undefined : getAcpProvider(providerKey);\n if (!isCustom && !provider && !options.allowUnknownServer) {\n return null;\n }\n\n // Undefined model → the *preferred* default (Vertex-safe for Gemini), not\n // the raw registry default — see getAcpPreferredDefaultModel.\n const model =\n options.model === undefined\n ? getAcpPreferredDefaultModel(providerKey)\n : options.model;\n\n // ``acp_args: []`` resets any API-set ``acp_args`` that would\n // otherwise survive and concatenate to ``acp_command`` at spawn time\n // (the agent-server merges the two before exec). Callers building the\n // payload from a textarea that already shows the merged command\n // (Settings → Agent) round-trip correctly — the merged tokens land in\n // ``acp_command`` here, so no args are lost.\n return {\n agent_kind: \"acp\",\n acp_server: providerKey,\n acp_command: options.command ?? [],\n acp_args: [],\n acp_model: model ?? null,\n };\n}\n"],"mappings":";;;AAkCA,SAAS,EAAa,GAA+B;AACnD,KAAI,OAAO,KAAU,SAAU,QAAO;CACtC,IAAM,IAAU,EAAM,MAAM;AAG5B,QAFI,CAAC,KACD,MAAA,gBAAyC,OACtC;;AAgBT,SAAgB,EAAyB,GAMvB;AAChB,MAAK,IAAM,KAAa;EACtB,EAAO;EACP,EAAO;EACP,EAAO;EACP,EAAO;EACR,EAAE;EACD,IAAM,IAAQ,EAAa,EAAU;AACrC,MAAI,EAAO,QAAO;;AAEpB,QAAO,EAAO,mBAAmB;;AAiEnC,IAAM,IAGF;CACF,eAAe;EACb,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACD,OAAO;EACL,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACD,cAAc;EACZ,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACF,EAMY,IAAqC,OAAO,QACvD,EACD,CAAC,KAAK,CAAC,GAAK,OAAQ;CACnB,IAAM,IAAO,EAAqB,EAAI;AACtC,QAAO;EACL;EACA,cAAc,GAAM,gBAAgB;EACpC,iBAAiB,IAAO,CAAC,GAAG,EAAK,gBAAgB,GAAG,EAAE;EACtD,kBAAkB,GAAM,kBAAkB,KAAK,OAAW;GACxD,IAAI,EAAM;GACV,OAAO,EAAM;GACd,EAAE;EACH,eAAe,GAAM,iBAAiB,KAAA;EACtC,iBAAiB,EAAG;EACpB,MAAM,EAAG;EACV;EACD;AAyDc,EAAQ,sCAQR,EAAQ,wCAQR,EAAQ,sCAOR,EAAQ,wCAIR,EAAQ,yCAIR,EAAQ;AAmDxB,IAAa,IAAwB;AAkBrC,SAAgB,EACd,GACe;AAEf,QADI,MAAQ,eAAqB,IAC1B,EAAe,EAAI,EAAE,iBAAiB;;AAoE/C,SAAgB,EACd,GAC+B;AAC1B,OACL,QAAO,EAAc,MAAM,MAAa,EAAS,QAAQ,EAAI;;AAmB/D,SAAgB,EACd,GACe;CACf,IAAM,IAAQ,EAAe,EAAI;AACjC,QAAO,IAAQ,EAAM,eAAe;;AAYtC,SAAgB,EACd,GACiB;AACjB,QAAO,EAAe,EAAI,EAAE,QAAA;;AAa9B,SAAgB,EACd,GACA,GACe;AAIf,QAHK,IACY,EAAe,EAClB,EAAU,kBAAkB,MAAM,MAAM,EAAE,OAAO,EAAQ,EACzD,SAAS,IAHF"}
1
+ {"version":3,"file":"acp-providers.js","names":[],"sources":["../../src/constants/acp-providers.ts"],"sourcesContent":["import { getAcpProvider as getClientAcpProvider } from \"@openhands/typescript-client\";\nimport { I18nKey } from \"#/i18n/declaration\";\n\nexport type ACPProviderIcon =\n | \"claude-code\"\n | \"codex\"\n | \"gemini\"\n | \"cli-generic\";\n\nexport const ACP_PROVIDER_FALLBACK_ICON: ACPProviderIcon = \"cli-generic\";\n\n// Sentinel ``agent.llm.model`` returned by older SDKs for ACP conversations\n// in lieu of a real model. Suppressed at every consumer that resolves a\n// display string.\n//\n// NB: ``\"default\"`` is NOT a placeholder. claude-agent-acp 0.44+ exposes\n// ``default`` (\"Default (recommended)\") as a real, selectable model in its\n// configOptions select — the server reports it as ``currentModelId`` and lists\n// it in ``availableModels`` — so it must surface like any other model id.\nexport const ACP_MANAGED_SENTINEL = \"acp-managed\";\n\n/**\n * Filter for \"real\" ACP model strings — non-empty and not the legacy\n * ``acp-managed`` sentinel. Returns the trimmed value on success, ``null``\n * otherwise.\n */\nfunction realAcpModel(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n if (trimmed === ACP_MANAGED_SENTINEL) return null;\n return trimmed;\n}\n\n/**\n * Single source of truth for resolving the model string to surface for an\n * ACP conversation/settings context. Consumed by the conversation adapter\n * (chip text), the conversation-creation path (concrete ``acp_model``\n * payload), the Settings → Agent form (initial value), and the chat-input\n * model label.\n *\n * Precedence: SDK runtime fields → user-configured ``acp_model`` →\n * legacy ``agent.llm.model`` → provider default (when ``providerDefault``\n * is passed). Pass ``providerDefault`` only on surfaces that should\n * silently substitute the registry default; omit it for the conversation\n * chip, which must distinguish \"no concrete model\" from \"default\".\n */\nexport function resolveEffectiveAcpModel(inputs: {\n runtimeName?: string | null;\n runtimeId?: string | null;\n configured?: string | null;\n sdkLlm?: string | null;\n providerDefault?: string | null;\n}): string | null {\n for (const candidate of [\n inputs.runtimeName,\n inputs.runtimeId,\n inputs.configured,\n inputs.sdkLlm,\n ]) {\n const value = realAcpModel(candidate);\n if (value) return value;\n }\n return inputs.providerDefault ?? null;\n}\n\n/**\n * Shape of a built-in ACP (Agent Client Protocol) provider as Canvas consumes\n * it. The data fields (display name, launch command, model picker + default)\n * are sourced at module load from ``@openhands/typescript-client``'s ACP\n * registry — the generated mirror of the Python source of truth\n * ``openhands.sdk.settings.acp_providers``. This config only adds the\n * Canvas-specific UI fields ({@link ACPProviderConfig.icon} +\n * {@link ACPProviderConfig.description_key}); see {@link ACP_PROVIDER_UI}.\n */\nexport interface ACPProviderConfig {\n /** Stable registry key, also stored on conversations as ``tags.acpserver``. */\n key: string;\n /** Human-readable name shown in dropdowns and conversation chips. */\n display_name: string;\n /**\n * Tokens passed to the agent-server as ``acp_command`` when this preset\n * is picked. Each entry must be a real ACP-protocol stdio server — the\n * SDK validates this against the {@link ACPProviderConfig.key}.\n *\n * NB: ``npx -y @openai/codex acp`` looks plausible but is **not** an\n * ACP server — the codex CLI has no ``acp`` subcommand and exits with\n * ``Error: stdin is not a terminal`` when spawned without a TTY, which\n * silently deadlocks the agent-server's ACP handshake. Use\n * ``@agentclientprotocol/codex-acp`` (the codex ACP wrapper) instead.\n */\n default_command: string[];\n /**\n * Suggested ACP model IDs for the provider's picker, sourced from the\n * typescript-client registry. Not authoritative access checks; users can\n * still enter a custom override in Settings -> Agent.\n */\n available_models?: ACPModelOption[];\n /** Model ID preselected for built-in providers so Canvas never saves blank. */\n default_model?: string;\n /**\n * i18n key for the one-line provider description rendered under the\n * onboarding tile. Stored on the registry so adding a new ACP\n * provider only requires editing this file (not the onboarding tile\n * list separately).\n */\n description_key: I18nKey;\n /**\n * Serializable icon key used by UI surfaces that render provider\n * choices. Kept as a string so the SDK mirror check can continue to\n * parse this registry without importing React components.\n */\n icon?: ACPProviderIcon;\n}\n\nexport interface ACPModelOption {\n /** Exact model ID sent as ``acp_model``. */\n id: string;\n /** Human-readable label shown in Settings -> Agent. */\n label: string;\n}\n\n// Canvas-only UI metadata per built-in provider, keyed by the ACP registry\n// key. Everything else — display name, launch command, model picker list and\n// default — comes from the typescript-client registry below. Adding a model\n// or a provider happens upstream in the SDK; Canvas only owns the brand icon\n// and the onboarding-tile description here. A provider with no entry here is\n// intentionally not surfaced in the UI.\nconst ACP_PROVIDER_UI: Record<\n string,\n { icon: ACPProviderIcon; description_key: I18nKey }\n> = {\n \"claude-code\": {\n icon: \"claude-code\",\n description_key: I18nKey.ONBOARDING$AGENT_CLAUDE_CODE_DESCRIPTION,\n },\n codex: {\n icon: \"codex\",\n description_key: I18nKey.ONBOARDING$AGENT_CODEX_DESCRIPTION,\n },\n \"gemini-cli\": {\n icon: \"gemini\",\n description_key: I18nKey.ONBOARDING$AGENT_GEMINI_CLI_DESCRIPTION,\n },\n};\n\n// Built-in ACP providers Canvas surfaces, built by enriching each upstream\n// registry record (``@openhands/typescript-client`` → Python SDK) with the\n// Canvas UI metadata above. Model lists + defaults are no longer hand-kept\n// here (closes agent-canvas#740) — they track the SDK via the pinned client.\nexport const ACP_PROVIDERS: ACPProviderConfig[] = Object.entries(\n ACP_PROVIDER_UI,\n).map(([key, ui]) => {\n const info = getClientAcpProvider(key);\n return {\n key,\n display_name: info?.display_name ?? key,\n default_command: info ? [...info.default_command] : [],\n available_models: info?.available_models?.map((model) => ({\n id: model.id,\n label: model.label,\n })),\n default_model: info?.default_model ?? undefined,\n description_key: ui.description_key,\n icon: ui.icon,\n };\n});\n\nexport const ACP_CUSTOM_PRESET_KEY = \"custom\";\n\n/**\n * A credential an ACP provider authenticates with, surfaced during onboarding\n * so the user can populate it without leaving the flow. The {@link name} is\n * both the global-secret name and the environment variable the agent-server\n * exports into the ACP subprocess — keeping them identical is what makes a\n * saved secret actually reach the provider CLI.\n */\nexport interface ACPProviderSecretField {\n /** Secret name and env var (e.g. ``\"ANTHROPIC_API_KEY\"``). Must satisfy the\n * secret-name pattern ``^[a-zA-Z][a-zA-Z0-9_]{0,63}$``. */\n name: string;\n /** Render as a masked password input (API keys, OAuth tokens, credential\n * blobs) rather than a plain-text input (base URLs, project IDs). Doubles as\n * \"this is an actual credential\": when the onboarding step is required, only\n * a ``secret`` field satisfies it — a base URL or GCP scalar alone can't\n * authenticate anything. */\n secret?: boolean;\n /** Render as a multi-line textarea rather than a single-line input. Set for\n * file-content credentials the user pastes verbatim (Codex ``auth.json``,\n * Gemini Vertex service-account / ADC JSON) — the ones the SDK materialises\n * to disk, which a cloud backend can't consume yet (agent-canvas#1016). */\n multiline?: boolean;\n /** i18n key for the one-line helper text under the field. */\n hint_key: I18nKey;\n /**\n * Interpolation values for {@link hint_key} (e.g. ``{ file: \"~/.codex/auth.json\" }``\n * for the shared \"paste the file contents\" hint). Omitted for hints that\n * take no parameters.\n */\n hint_values?: Record<string, string>;\n}\n\n/**\n * Container credentials — beyond the registry's API key / base URL — that\n * canvas collects per provider so a *containerized* agent-server (no host login\n * state) can authenticate the ACP CLI: subscription tokens plus the file-content\n * blobs and Vertex config the SDK's ``acp_file_secrets`` materialisation\n * consumes (agent-canvas#1013/#1014). They have no registry env-var entry\n * because they're a deployment concern, not a model registry field. The Gemini\n * project/location/flag are plain config, not secrets — grouped here because\n * they travel with the SA blob.\n *\n * The blob *names* (``CODEX_AUTH_JSON`` / ``GOOGLE_APPLICATION_CREDENTIALS_JSON``)\n * duplicate the SDK registry's ``file_secrets`` specs; derive them from the\n * client registry once the pinned ``@openhands/typescript-client`` mirrors that\n * field (same bump that unblocks ``acp_isolate_data_dir``, see #1019).\n */\nconst ACP_RESERVED_CREDENTIALS: Record<string, ACPProviderSecretField[]> = {\n codex: [\n {\n name: \"CODEX_AUTH_JSON\",\n secret: true,\n multiline: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_FILE_BLOB_HINT,\n hint_values: { file: \"~/.codex/auth.json\" },\n },\n ],\n \"claude-code\": [\n {\n name: \"CLAUDE_CODE_OAUTH_TOKEN\",\n secret: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_OAUTH_TOKEN_HINT,\n },\n ],\n \"gemini-cli\": [\n {\n name: \"GOOGLE_APPLICATION_CREDENTIALS_JSON\",\n secret: true,\n multiline: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_FILE_BLOB_HINT,\n hint_values: {\n file: \"~/.config/gcloud/application_default_credentials.json\",\n },\n },\n {\n name: \"GOOGLE_CLOUD_PROJECT\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_GCP_PROJECT_HINT,\n },\n {\n name: \"GOOGLE_CLOUD_LOCATION\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_GCP_LOCATION_HINT,\n },\n {\n name: \"GOOGLE_GENAI_USE_VERTEXAI\",\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_VERTEXAI_FLAG_HINT,\n },\n ],\n};\n\n/**\n * Credential pairs that break each other at runtime, keyed by provider —\n * mirrors the SDK's ``_ENV_CONFLICT_MAP`` (software-agent-sdk#3588). Claude's\n * OAuth token (``CLAUDE_CODE_OAUTH_TOKEN``) authenticates against Anthropic\n * directly, so when it is set the SDK strips:\n * - ``ANTHROPIC_API_KEY`` — otherwise it takes precedence and silently\n * bypasses the subscription;\n * - ``ANTHROPIC_BASE_URL`` — otherwise it proxies the bearer to an endpoint\n * that rejects it (see docs/ACP_AGENTS.md).\n * Either one set alongside the token is silently ignored at runtime, so warn.\n */\nconst ACP_CREDENTIAL_CONFLICTS: Record<string, Array<[string, string]>> = {\n \"claude-code\": [\n [\"CLAUDE_CODE_OAUTH_TOKEN\", \"ANTHROPIC_API_KEY\"],\n [\"CLAUDE_CODE_OAUTH_TOKEN\", \"ANTHROPIC_BASE_URL\"],\n ],\n};\n\n/**\n * The ``[credential, conflicting]`` pairs the user currently has set for\n * ``providerKey``. ``hasValue`` should cover both typed and already-saved\n * values, since a previously saved secret conflicts just the same.\n */\nexport function getAcpCredentialConflicts(\n key: string | null | undefined,\n hasValue: (name: string) => boolean,\n): Array<[string, string]> {\n if (!key) return [];\n return (ACP_CREDENTIAL_CONFLICTS[key] ?? []).filter(\n ([credential, conflicting]) =>\n hasValue(credential) && hasValue(conflicting),\n );\n}\n\n/**\n * Vertex AI-safe ``acp_model`` for Gemini.\n *\n * Left unset, gemini-cli falls back to its own internal default — a preview\n * flash model that 404s on Google Cloud projects without preview access. And a\n * ``*-flash`` id is no safer to pin: gemini-cli re-resolves flash ids at\n * generation time to its *current default* flash (software-agent-sdk#3532), so\n * a pinned ``gemini-2.5-flash`` still ran ``gemini-3-flash``. Only a non-flash\n * id sticks; ``gemini-2.5-pro`` is broadly available on Vertex and the\n * API-key / Google-login paths, so canvas preselects it. Kept as a named\n * constant so every model-default consumer agrees.\n */\nexport const ACP_VERTEX_SAFE_MODEL = \"gemini-2.5-pro\";\n\n/**\n * The default ``acp_model`` canvas substitutes for ``providerKey`` wherever no\n * concrete model is configured — settings seeding (onboarding, Settings →\n * Agent), the {@link buildAcpAgentSettingsDiff} fallback, and the start-request\n * fallback for a saved ``null``. Overrides only Gemini (→\n * {@link ACP_VERTEX_SAFE_MODEL}, see why above); every other provider keeps its\n * registry {@link ACPProviderConfig.default_model}. Returns ``null`` when\n * there's no override and no registry default, letting the ACP server pick its\n * own.\n *\n * Distinct from {@link ACPProviderConfig.default_model} (which mirrors the SDK\n * registry verbatim, closing agent-canvas#740): this is the *preferred* default,\n * deliberately diverging for Gemini where the registry value isn't safe on\n * every backend — so every default-model surface must route through this, not\n * read ``default_model`` directly.\n */\nexport function getAcpPreferredDefaultModel(\n key: string | null | undefined,\n): string | null {\n if (key === \"gemini-cli\") return ACP_VERTEX_SAFE_MODEL;\n return getAcpProvider(key)?.default_model ?? null;\n}\n\n/**\n * List the credentials Canvas should prompt for when onboarding the given ACP\n * provider. The API-key and base-URL field *names* track the SDK registry's\n * ``api_key_env_var`` / ``base_url_env_var`` (mirrored via\n * ``@openhands/typescript-client``) so they can't drift as providers are added\n * or renamed; the per-provider container credentials (subscription / Vertex\n * blobs) come from {@link ACP_RESERVED_CREDENTIALS}, since those are a\n * containerized-deployment concern with no model-registry entry. Each field\n * name equals the env var the agent-server exports into the provider subprocess\n * (which is what makes a saved secret reach the CLI). On the wire none of this\n * is special — every saved credential travels uniformly as a ``LookupSecret``.\n *\n * Field order is: container subscription/Vertex credentials → API key →\n * optional base URL. Every field is optional at the UI level — the step is\n * skippable, and a subscription / OAuth login on the backend takes precedence\n * over a key at runtime. (Whether the *step* is required is a\n * backend-capability decision the onboarding modal makes; see\n * ``backendRequiresAcpCredentials``.)\n *\n * NB: the base URL is rendered plain-text (not ``secret``), so it never counts\n * toward a required credential step — setting ``ANTHROPIC_BASE_URL`` alongside\n * a ``CLAUDE_CODE_OAUTH_TOKEN`` breaks the token's bearer auth, which the forms\n * surface via {@link getAcpCredentialConflicts}.\n *\n * Returns ``[]`` for OpenHands, the ``\"custom\"`` preset, any unknown key, and a\n * future OAuth-only provider whose registry entry has no ``api_key_env_var`` —\n * callers treat an empty list as \"no credentials step for this provider\".\n */\nexport function getAcpProviderSecrets(\n key: string | null | undefined,\n): ACPProviderSecretField[] {\n if (!key) return [];\n const info = getClientAcpProvider(key);\n if (!info) return [];\n // Subscription / Vertex credentials first — they're the primary auth path for\n // ACP providers (Claude Pro/Max OAuth token, Codex ChatGPT auth.json), with\n // the API key as the fallback below.\n const fields: ACPProviderSecretField[] = [\n ...(ACP_RESERVED_CREDENTIALS[key] ?? []),\n ];\n if (info.api_key_env_var) {\n fields.push({\n name: info.api_key_env_var,\n secret: true,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_API_KEY_HINT,\n });\n }\n if (info.base_url_env_var) {\n fields.push({\n name: info.base_url_env_var,\n hint_key: I18nKey.ONBOARDING$ACP_SECRET_BASE_URL_HINT,\n });\n }\n return fields;\n}\n\n/**\n * Look up a built-in ACP provider config by its registry key.\n *\n * Returns ``undefined`` for an empty / null key, for the ``\"custom\"`` preset\n * (which has no registry entry), and for any forward-compatible key Canvas's\n * registry doesn't know about yet. Centralizes the ``ACP_PROVIDERS.find(...)``\n * lookup shared by the resolvers below and by the adapter / settings surfaces\n * so the key-comparison shape lives in one place.\n */\nexport function getAcpProvider(\n key: string | null | undefined,\n): ACPProviderConfig | undefined {\n if (!key) return undefined;\n return ACP_PROVIDERS.find((provider) => provider.key === key);\n}\n\n/**\n * Resolve an ACP provider registry key (the value stored under\n * ``tags.acpserver`` on a conversation) to a human display name for the\n * sidebar chip.\n *\n * Returns ``null`` for an empty / null key and for keys not in\n * {@link ACP_PROVIDERS} — most notably ``\"custom\"`` (the user-supplied\n * command preset has no canonical brand name) and any forward-compatible\n * value Canvas's registry doesn't know about yet. Callers should fall\n * back to a generic ``\"ACP\"`` label in that case so the chip still\n * communicates \"this is an ACP conversation\".\n *\n * Kept separate from {@link buildAcpAgentSettingsDiff}'s lookup so the\n * conversation-card render path can resolve display names without\n * importing the settings-payload builder.\n */\nexport function getAcpProviderDisplayName(\n key: string | null | undefined,\n): string | null {\n const found = getAcpProvider(key);\n return found ? found.display_name : null;\n}\n\n/**\n * Resolve an ACP provider registry key to the icon discriminator the\n * conversation chip should render alongside the model text.\n *\n * Falls back to {@link ACP_PROVIDER_FALLBACK_ICON} for ``\"custom\"``,\n * unknown keys, or a missing key — the chip then shows a neutral\n * terminal glyph that still communicates \"this is an ACP conversation\"\n * without claiming a brand identity we don't know.\n */\nexport function resolveAcpProviderIcon(\n key: string | null | undefined,\n): ACPProviderIcon {\n return getAcpProvider(key)?.icon ?? ACP_PROVIDER_FALLBACK_ICON;\n}\n\n/**\n * Resolve a raw ``acp_model`` ID to the human-readable label the provider's\n * picker shows for it (e.g. ``\"claude-opus-4-7\"`` → ``\"Claude Opus 4.7\"``).\n *\n * Falls back to the raw ID when the provider is unknown or the ID isn't one\n * of its registered {@link ACPModelOption}s — so a user's custom override\n * still renders something meaningful rather than nothing. Returns ``null``\n * only when there is no model to show, letting the conversation chip decide\n * to display the provider name instead.\n */\nexport function labelForAcpModel(\n serverKey: string | null | undefined,\n modelId: string | null | undefined,\n): string | null {\n if (!modelId) return null;\n const provider = getAcpProvider(serverKey);\n const match = provider?.available_models?.find((m) => m.id === modelId);\n return match?.label ?? modelId;\n}\n\n/**\n * Build the ``agent_settings_diff`` payload PATCH /api/settings expects\n * for the agent-kind/provider choice the user just made.\n *\n * Used by both the Settings → Agent page and the onboarding \"choose\n * agent\" step — keeping the shape in one helper means a future change\n * (e.g. always seeding ``acp_command`` from the registry instead of\n * sending ``[]``, or adding new ``acp_*`` reset fields) lands in both\n * surfaces atomically.\n *\n * Returns ``null`` for an unknown ACP provider key by default — the\n * caller can skip the save (the UI shouldn't surface unknown options,\n * but the defensive path keeps a buggy preset list from corrupting\n * settings).\n *\n * Pass ``allowUnknownServer: true`` to opt into pass-through for keys\n * that aren't in {@link ACP_PROVIDERS} or ``ACP_CUSTOM_PRESET_KEY``.\n * The Settings → Agent page uses this when the user opens settings\n * that already carry an ``acp_server`` value canvas's registry\n * doesn't know about (e.g. set out-of-band via the API for a provider\n * we haven't mirrored yet) and saves without changing the command —\n * otherwise the original key would be silently demoted to ``\"custom\"``.\n */\nexport function buildAcpAgentSettingsDiff(\n providerKey: string,\n options: {\n command?: string[];\n model?: string | null;\n allowUnknownServer?: boolean;\n } = {},\n): Record<string, unknown> | null {\n if (providerKey === \"openhands\") {\n // Switching back to OpenHands. The agent-server's ``Settings.update``\n // applies a fresh ``{'agent_kind': ...}`` base whenever the kind\n // flips, so any ``acp_*`` fields would be discarded before\n // validation. Send the kind alone.\n return { agent_kind: \"openhands\" };\n }\n\n const isCustom = providerKey === ACP_CUSTOM_PRESET_KEY;\n const provider = isCustom ? undefined : getAcpProvider(providerKey);\n if (!isCustom && !provider && !options.allowUnknownServer) {\n return null;\n }\n\n // Undefined model → the *preferred* default (Vertex-safe for Gemini), not\n // the raw registry default — see getAcpPreferredDefaultModel.\n const model =\n options.model === undefined\n ? getAcpPreferredDefaultModel(providerKey)\n : options.model;\n\n // ``acp_args: []`` resets any API-set ``acp_args`` that would\n // otherwise survive and concatenate to ``acp_command`` at spawn time\n // (the agent-server merges the two before exec). Callers building the\n // payload from a textarea that already shows the merged command\n // (Settings → Agent) round-trip correctly — the merged tokens land in\n // ``acp_command`` here, so no args are lost.\n return {\n agent_kind: \"acp\",\n acp_server: providerKey,\n acp_command: options.command ?? [],\n acp_args: [],\n acp_model: model ?? null,\n };\n}\n"],"mappings":";;;AA0BA,SAAS,EAAa,GAA+B;AACnD,KAAI,OAAO,KAAU,SAAU,QAAO;CACtC,IAAM,IAAU,EAAM,MAAM;AAG5B,QAFI,CAAC,KACD,MAAA,gBAAyC,OACtC;;AAgBT,SAAgB,EAAyB,GAMvB;AAChB,MAAK,IAAM,KAAa;EACtB,EAAO;EACP,EAAO;EACP,EAAO;EACP,EAAO;EACR,EAAE;EACD,IAAM,IAAQ,EAAa,EAAU;AACrC,MAAI,EAAO,QAAO;;AAEpB,QAAO,EAAO,mBAAmB;;AAiEnC,IAAM,IAGF;CACF,eAAe;EACb,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACD,OAAO;EACL,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACD,cAAc;EACZ,MAAM;EACN,iBAAiB,EAAQ;EAC1B;CACF,EAMY,IAAqC,OAAO,QACvD,EACD,CAAC,KAAK,CAAC,GAAK,OAAQ;CACnB,IAAM,IAAO,EAAqB,EAAI;AACtC,QAAO;EACL;EACA,cAAc,GAAM,gBAAgB;EACpC,iBAAiB,IAAO,CAAC,GAAG,EAAK,gBAAgB,GAAG,EAAE;EACtD,kBAAkB,GAAM,kBAAkB,KAAK,OAAW;GACxD,IAAI,EAAM;GACV,OAAO,EAAM;GACd,EAAE;EACH,eAAe,GAAM,iBAAiB,KAAA;EACtC,iBAAiB,EAAG;EACpB,MAAM,EAAG;EACV;EACD;AAyDc,EAAQ,sCAQR,EAAQ,wCAQR,EAAQ,sCAOR,EAAQ,wCAIR,EAAQ,yCAIR,EAAQ;AAmDxB,IAAa,IAAwB;AAkBrC,SAAgB,EACd,GACe;AAEf,QADI,MAAQ,eAAqB,IAC1B,EAAe,EAAI,EAAE,iBAAiB;;AAoE/C,SAAgB,EACd,GAC+B;AAC1B,OACL,QAAO,EAAc,MAAM,MAAa,EAAS,QAAQ,EAAI;;AAmB/D,SAAgB,EACd,GACe;CACf,IAAM,IAAQ,EAAe,EAAI;AACjC,QAAO,IAAQ,EAAM,eAAe;;AAYtC,SAAgB,EACd,GACiB;AACjB,QAAO,EAAe,EAAI,EAAE,QAAA;;AAa9B,SAAgB,EACd,GACA,GACe;AAIf,QAHK,IACY,EAAe,EAClB,EAAU,kBAAkB,MAAM,MAAM,EAAE,OAAO,EAAQ,EACzD,SAAS,IAHF"}
@@ -1,4 +1,4 @@
1
- const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`../stores/browser-store.cjs`),n=require(`../stores/command-store.cjs`),r=require(`../utils/conversation-local-storage.cjs`),i=require(`../stores/conversation-store.cjs`),ee=require(`../stores/conversation-state-store.cjs`),te=require(`../node_modules/@openhands/typescript-client/dist/client/conversation-client.cjs`);require(`../node_modules/@openhands/typescript-client/dist/clients.cjs`);const a=require(`../utils/websocket-url.cjs`),ne=require(`../api/agent-server-client-options.cjs`),re=require(`../node_modules/@tanstack/react-query/build/modern/QueryClientProvider.cjs`),o=require(`../types/agent-server/type-guards.cjs`),s=require(`../stores/use-event-store.cjs`),ie=require(`../hooks/use-websocket.cjs`),ae=require(`../constants/server-connection-error.cjs`),oe=require(`../stores/error-message-store.cjs`),se=require(`../stores/optimistic-user-message-store.cjs`),ce=require(`../stores/goal-store.cjs`),le=require(`../services/canvas-ui.cjs`),ue=require(`../utils/cache-utils.cjs`),de=require(`../api/event-service/event-service.api.cjs`),c=require(`../utils/error-handler.cjs`),l=require(`../api/conversation-metadata-store.cjs`),fe=require(`../hooks/mutation/use-read-conversation-file.cjs`),pe=require(`../stores/metrics-store.cjs`),me=require(`../hooks/query/use-conversation-history.cjs`),u=require(`../hooks/chat/record-model-switch-message.cjs`),d=require(`../hooks/mutation/conversation-mutation-utils.cjs`);let f=require(`react`);f=e.__toESM(f,1);let he=require(`react/jsx-runtime`);var p=(0,f.createContext)(void 0);function m(e){return e.llm_message.content.filter(e=>e.type===`text`).map(e=>e.text).join(``)}function h({children:e,conversationId:h,conversationUrl:g,sessionApiKey:_,subConversations:v,subConversationIds:ge}){let[y,b]=(0,f.useState)(`CONNECTING`),[x,S]=(0,f.useState)(`CONNECTING`),C=f.default.useRef(!1),w=f.default.useRef(!1),T=re.useQueryClient(),E=s.useEventStore(e=>e.addEvent),_e=s.useEventStore(e=>e.addEvents),D=s.useEventStore(e=>e.clearEventsForConversation),{setErrorMessage:O,removeErrorMessage:k,clearConnectionError:A}=oe.useErrorMessageStore(),j=se.useOptimisticUserMessageStore(e=>e.consumeMatchingPendingMessage),{setExecutionStatus:M}=ee.useConversationStateStore(),{appendInput:N,appendOutput:P}=n.useCommandStore(),F=t.useBrowserStore(e=>e.reset),[I,L]=(0,f.useState)(!0),[R,z]=(0,f.useState)(null),{setPlanContent:B}=i.useConversationStore(),{mutate:V}=fe.useReadConversationFile(),H=(0,f.useRef)(0),U=(0,f.useRef)(null),ve=e=>e?.toUpperCase().endsWith(`PLAN.MD`)??!1,W=(0,f.useCallback)(()=>{A()},[A]),G=(0,f.useCallback)(e=>{if(e.value.usage_to_metrics?.agent){let t=e.value.usage_to_metrics.agent,n={cost:t.accumulated_cost,max_budget_per_task:t.max_budget_per_task??null,usage:t.accumulated_token_usage?{prompt_tokens:t.accumulated_token_usage.prompt_tokens,completion_tokens:t.accumulated_token_usage.completion_tokens,cache_read_tokens:t.accumulated_token_usage.cache_read_tokens,cache_write_tokens:t.accumulated_token_usage.cache_write_tokens,context_window:t.accumulated_token_usage.context_window,per_turn_token:t.accumulated_token_usage.per_turn_token}:null};pe.default.getState().setMetrics(n)}},[]),{data:K,isPending:ye,isFetching:q,isError:be}=me.useConversationHistory(h),xe=!!h&&ye;(0,f.useLayoutEffect)(()=>{let e=h??null;s.useEventStore.getState().loadedConversationId!==e&&(D(e),F())},[h,D,F]),(0,f.useLayoutEffect)(()=>{if(!(!K||K.events.length===0)&&(_e(K.events),h)){u.seedModelSwitchesFromHistory(h,s.useEventStore.getState().uiEvents);for(let e of K.events)o.isUserMessageEvent(e)&&j(h,m(e))}},[K,_e,h,j]);let J=(0,f.useMemo)(()=>{if(q)return null;let e=K?.events??[],t=e[e.length-1];return!t||!(`timestamp`in t)||!t.timestamp?null:t.timestamp},[K,q]),Y=(0,f.useMemo)(()=>!h||!g||q&&!be?null:a.buildWebSocketUrl(h,g),[h,g,q,be]),X=(0,f.useMemo)(()=>{if(!v?.length)return null;let e=v[0];return!e?.id||!e.conversation_url?null:a.buildWebSocketUrl(e.id,e.conversation_url)},[v]),Se=(0,f.useMemo)(()=>X?y===`CONNECTING`||x===`CONNECTING`?`CONNECTING`:y===`OPEN`&&x===`OPEN`?`OPEN`:y===`CLOSED`&&x===`CLOSED`?`CLOSED`:y===`CLOSING`||x===`CLOSING`?`CLOSING`:`CLOSED`:y,[y,x,X]);(0,f.useEffect)(()=>{R!==null&&H.current>=R&&I&&L(!1)},[R,I,H]),(0,f.useEffect)(()=>{if(!I&&U.current){let{path:e,conversationId:t}=U.current;V({conversationId:t,filePath:e},{onSuccess:e=>{B(e)},onError:e=>{console.warn(`Failed to read conversation file:`,e)}}),U.current=null}},[I,V,B]),(0,f.useEffect)(()=>{C.current=!1,L(!!ge?.length),z(null),H.current=0,U.current=null},[ge]),(0,f.useEffect)(()=>{C.current=!1,w.current=!1,U.current=null},[h]);let Z=(0,f.useMemo)(()=>xe||I,[xe,I]),Ce=(0,f.useCallback)(e=>{try{let n=JSON.parse(e.data);if(o.isAgentServerEvent(n)){let e=!s.useEventStore.getState().eventIds.has(n.id)&&o.isSwitchLLMObservationEvent(n)?n:null;if(E(n),o.isDisplayableErrorEvent(n)){let e=n;c.trackError({message:e.detail,source:`conversation`,metadata:{eventId:e.id,errorCode:e.code}}),O(e.detail,`conversation`,e.code)}else W();if(o.isAgentErrorEvent(n)&&c.trackError({message:n.error,source:`agent`,metadata:{eventId:n.id,toolName:n.tool_name,toolCallId:n.tool_call_id}}),o.isUserMessageEvent(n)&&h&&(j(h,m(n)),r.setConversationState(h,{draftMessage:null})),o.isActionEvent(n)&&ue.handleActionEventCacheInvalidation(n,h||`test-conversation-id`,T),o.isConversationStateUpdateEvent(n)&&(o.isFullStateConversationStateUpdateEvent(n)&&M(n.value.execution_status),o.isAgentStatusConversationStateUpdateEvent(n)&&M(n.value),o.isStatsConversationStateUpdateEvent(n)&&G(n),o.isGoalConversationStateUpdateEvent(n)&&h&&ce.useGoalStore.getState().setStatus(h,n.value)),o.isExecuteBashActionEvent(n)&&N(n.action.command),o.isExecuteBashObservationEvent(n)&&P(n.observation.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
2
- `)),o.isBrowserObservationEvent(n)){let{screenshot_data:e}=n.observation;if(e){let n=e.startsWith(`data:`)?e:`data:image/png;base64,${e}`;t.useBrowserStore.getState().setScreenshotSrc(n)}}if(o.isBrowserNavigateActionEvent(n)&&t.useBrowserStore.getState().setUrl(n.action.url),h&&e&&!e.observation.is_error){u.recordModelSwitchMessage(h,e.observation.profile_name);let t=l.getStoredConversationMetadata(h);l.setStoredConversationMetadata(h,{selected_repository:t?.selected_repository??null,selected_branch:t?.selected_branch??null,git_provider:t?.git_provider??null,selected_workspace:t?.selected_workspace??null,active_profile:e.observation.profile_name,plugins:t?.plugins??null}),e.observation.active_model&&d.updateConversationLlmModelInCache(T,h,e.observation.active_model),d.invalidateConversationQueries(T,h)}o.isCanvasUIActionEvent(n)&&le.handleCanvasUIAction(n.action,h??null)}}catch(e){console.warn(`Failed to parse WebSocket message as JSON:`,e)}},[E,O,j,T,h,M,N,P,G,W]),we=(0,f.useCallback)(e=>{try{let t=JSON.parse(e.data);if(I&&(H.current+=1,R!==null&&H.current>=R&&L(!1)),o.isAgentServerEvent(t)){if(E({...t,isFromPlanningAgent:!0}),o.isDisplayableErrorEvent(t)){let e=t;c.trackError({message:e.detail,source:`planning_conversation`,metadata:{eventId:e.id,errorCode:e.code}}),O(e.detail,`conversation`,e.code)}else W();if(o.isAgentErrorEvent(t)&&c.trackError({message:t.error,source:`planning_agent`,metadata:{eventId:t.id,toolName:t.tool_name,toolCallId:t.tool_call_id}}),o.isUserMessageEvent(t)&&h&&(j(h,m(t)),r.setConversationState(h,{draftMessage:null})),o.isActionEvent(t)&&ue.handleActionEventCacheInvalidation(t,v?.[0]?.id||`test-conversation-id`,T),o.isConversationStateUpdateEvent(t)&&(o.isFullStateConversationStateUpdateEvent(t)&&M(t.value.execution_status),o.isAgentStatusConversationStateUpdateEvent(t)&&M(t.value),o.isStatsConversationStateUpdateEvent(t)&&G(t),o.isGoalConversationStateUpdateEvent(t)&&h&&ce.useGoalStore.getState().setStatus(h,t.value)),o.isExecuteBashActionEvent(t)&&N(t.action.command),o.isExecuteBashObservationEvent(t)&&P(t.observation.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
1
+ const e=require(`../_virtual/_rolldown/runtime.cjs`),t=require(`../stores/browser-store.cjs`),n=require(`../stores/command-store.cjs`),r=require(`../utils/conversation-local-storage.cjs`),i=require(`../stores/conversation-store.cjs`),ee=require(`../stores/conversation-state-store.cjs`),te=require(`../node_modules/@openhands/typescript-client/dist/client/conversation-client.cjs`);require(`../node_modules/@openhands/typescript-client/dist/clients.cjs`);const a=require(`../utils/websocket-url.cjs`),ne=require(`../api/agent-server-client-options.cjs`),re=require(`../node_modules/@tanstack/react-query/build/modern/QueryClientProvider.cjs`),o=require(`../types/agent-server/type-guards.cjs`),s=require(`../stores/use-event-store.cjs`),ie=require(`../hooks/use-websocket.cjs`),ae=require(`../constants/server-connection-error.cjs`),oe=require(`../stores/error-message-store.cjs`),se=require(`../stores/optimistic-user-message-store.cjs`),ce=require(`../stores/goal-store.cjs`),le=require(`../services/canvas-ui.cjs`),ue=require(`../utils/cache-utils.cjs`),de=require(`../api/event-service/event-service.api.cjs`),c=require(`../utils/error-handler.cjs`),l=require(`../api/conversation-metadata-store.cjs`),fe=require(`../hooks/mutation/use-read-conversation-file.cjs`),pe=require(`../stores/metrics-store.cjs`),me=require(`../hooks/query/use-conversation-history.cjs`),u=require(`../hooks/chat/record-model-switch-message.cjs`),d=require(`../hooks/mutation/conversation-mutation-utils.cjs`);let f=require(`react`);f=e.__toESM(f,1);let he=require(`react/jsx-runtime`);var p=(0,f.createContext)(void 0);function m(e){return e.llm_message.content.filter(e=>e.type===`text`).map(e=>e.text).join(``)}function h({children:e,conversationId:h,conversationUrl:g,sessionApiKey:_,subConversations:v,subConversationIds:ge}){let[y,b]=(0,f.useState)(`CONNECTING`),[x,S]=(0,f.useState)(`CONNECTING`),C=f.default.useRef(!1),w=f.default.useRef(!1),T=re.useQueryClient(),E=s.useEventStore(e=>e.addEvent),_e=s.useEventStore(e=>e.addEvents),D=s.useEventStore(e=>e.clearEventsForConversation),{setErrorMessage:O,removeErrorMessage:k,clearConnectionError:A}=oe.useErrorMessageStore(),j=se.useOptimisticUserMessageStore(e=>e.consumeMatchingPendingMessage),{setExecutionStatus:M}=ee.useConversationStateStore(),{appendInput:N,appendOutput:P}=n.useCommandStore(),F=t.useBrowserStore(e=>e.reset),[I,L]=(0,f.useState)(!0),[R,z]=(0,f.useState)(null),{setPlanContent:B}=i.useConversationStore(),{mutate:V}=fe.useReadConversationFile(),H=(0,f.useRef)(0),U=(0,f.useRef)(null),ve=e=>e?.toUpperCase().endsWith(`PLAN.MD`)??!1,W=(0,f.useCallback)(()=>{A()},[A]),G=(0,f.useCallback)(e=>{if(e.value.usage_to_metrics?.agent){let t=e.value.usage_to_metrics.agent,n={cost:t.accumulated_cost,max_budget_per_task:t.max_budget_per_task??null,usage:t.accumulated_token_usage?{prompt_tokens:t.accumulated_token_usage.prompt_tokens,completion_tokens:t.accumulated_token_usage.completion_tokens,cache_read_tokens:t.accumulated_token_usage.cache_read_tokens,cache_write_tokens:t.accumulated_token_usage.cache_write_tokens,context_window:t.accumulated_token_usage.context_window,per_turn_token:t.accumulated_token_usage.per_turn_token}:null};pe.default.getState().setMetrics(n)}},[]),{data:K,isPending:ye,isFetching:q,isError:be}=me.useConversationHistory(h),xe=!!h&&ye;(0,f.useLayoutEffect)(()=>{let e=h??null;s.useEventStore.getState().loadedConversationId!==e&&(D(e),F())},[h,D,F]),(0,f.useLayoutEffect)(()=>{if(!(!K||K.events.length===0)&&(_e(K.events),h)){u.seedModelSwitchesFromHistory(h,s.useEventStore.getState().uiEvents);for(let e of K.events)o.isUserMessageEvent(e)&&j(h,m(e))}},[K,_e,h,j]);let J=(0,f.useMemo)(()=>{if(q)return null;let e=K?.events??[],t=e[e.length-1];return!t||!(`timestamp`in t)||!t.timestamp?null:t.timestamp},[K,q]),Y=(0,f.useMemo)(()=>!h||!g||q&&!be?null:a.buildWebSocketUrl(h,g),[h,g,q,be]),X=(0,f.useMemo)(()=>{if(!v?.length)return null;let e=v[0];return!e?.id||!e.conversation_url?null:a.buildWebSocketUrl(e.id,e.conversation_url)},[v]),Se=(0,f.useMemo)(()=>X?y===`CONNECTING`||x===`CONNECTING`?`CONNECTING`:y===`OPEN`&&x===`OPEN`?`OPEN`:y===`CLOSED`&&x===`CLOSED`?`CLOSED`:y===`CLOSING`||x===`CLOSING`?`CLOSING`:`CLOSED`:y,[y,x,X]);(0,f.useEffect)(()=>{R!==null&&H.current>=R&&I&&L(!1)},[R,I,H]),(0,f.useEffect)(()=>{if(!I&&U.current){let{path:e,conversationId:t}=U.current;V({conversationId:t,filePath:e},{onSuccess:e=>{B(e)},onError:e=>{console.warn(`Failed to read conversation file:`,e)}}),U.current=null}},[I,V,B]),(0,f.useEffect)(()=>{C.current=!1,L(!!ge?.length),z(null),H.current=0,U.current=null},[ge]),(0,f.useEffect)(()=>{C.current=!1,w.current=!1,U.current=null},[h]);let Z=(0,f.useMemo)(()=>xe||I,[xe,I]),Ce=(0,f.useCallback)(e=>{try{let n=JSON.parse(e.data);if(o.isAgentServerEvent(n)){let e=s.useEventStore.getState().eventIds.has(n.id),i=o.isSwitchLLMObservationEvent(n)?n:null;if(E(n),e)return;if(o.isDisplayableErrorEvent(n)){let e=n;c.trackError({message:e.detail,source:`conversation`,metadata:{eventId:e.id,errorCode:e.code}}),O(e.detail,`conversation`,e.code)}else W();if(o.isAgentErrorEvent(n)&&c.trackError({message:n.error,source:`agent`,metadata:{eventId:n.id,toolName:n.tool_name,toolCallId:n.tool_call_id}}),o.isUserMessageEvent(n)&&h&&(j(h,m(n)),r.setConversationState(h,{draftMessage:null})),o.isActionEvent(n)&&ue.handleActionEventCacheInvalidation(n,h||`test-conversation-id`,T),o.isConversationStateUpdateEvent(n)&&(o.isFullStateConversationStateUpdateEvent(n)&&M(n.value.execution_status),o.isAgentStatusConversationStateUpdateEvent(n)&&M(n.value),o.isStatsConversationStateUpdateEvent(n)&&G(n),o.isGoalConversationStateUpdateEvent(n)&&h&&ce.useGoalStore.getState().setStatus(h,n.value)),o.isExecuteBashActionEvent(n)&&N(n.action.command),o.isExecuteBashObservationEvent(n)&&P(n.observation.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
2
+ `)),o.isBrowserObservationEvent(n)){let{screenshot_data:e}=n.observation;if(e){let n=e.startsWith(`data:`)?e:`data:image/png;base64,${e}`;t.useBrowserStore.getState().setScreenshotSrc(n)}}if(o.isBrowserNavigateActionEvent(n)&&t.useBrowserStore.getState().setUrl(n.action.url),h&&i&&!i.observation.is_error){u.recordModelSwitchMessage(h,i.observation.profile_name);let e=l.getStoredConversationMetadata(h);l.setStoredConversationMetadata(h,{selected_repository:e?.selected_repository??null,selected_branch:e?.selected_branch??null,git_provider:e?.git_provider??null,selected_workspace:e?.selected_workspace??null,active_profile:i.observation.profile_name,plugins:e?.plugins??null}),i.observation.active_model&&d.updateConversationLlmModelInCache(T,h,i.observation.active_model),d.invalidateConversationQueries(T,h)}o.isCanvasUIActionEvent(n)&&le.handleCanvasUIAction(n.action,h??null)}}catch(e){console.warn(`Failed to parse WebSocket message as JSON:`,e)}},[E,O,j,T,h,M,N,P,G,W]),we=(0,f.useCallback)(e=>{try{let t=JSON.parse(e.data);if(I&&(H.current+=1,R!==null&&H.current>=R&&L(!1)),o.isAgentServerEvent(t)){let e=s.useEventStore.getState().eventIds.has(t.id);if(E({...t,isFromPlanningAgent:!0}),e)return;if(o.isDisplayableErrorEvent(t)){let e=t;c.trackError({message:e.detail,source:`planning_conversation`,metadata:{eventId:e.id,errorCode:e.code}}),O(e.detail,`conversation`,e.code)}else W();if(o.isAgentErrorEvent(t)&&c.trackError({message:t.error,source:`planning_agent`,metadata:{eventId:t.id,toolName:t.tool_name,toolCallId:t.tool_call_id}}),o.isUserMessageEvent(t)&&h&&(j(h,m(t)),r.setConversationState(h,{draftMessage:null})),o.isActionEvent(t)&&ue.handleActionEventCacheInvalidation(t,v?.[0]?.id||`test-conversation-id`,T),o.isConversationStateUpdateEvent(t)&&(o.isFullStateConversationStateUpdateEvent(t)&&M(t.value.execution_status),o.isAgentStatusConversationStateUpdateEvent(t)&&M(t.value),o.isStatsConversationStateUpdateEvent(t)&&G(t),o.isGoalConversationStateUpdateEvent(t)&&h&&ce.useGoalStore.getState().setStatus(h,t.value)),o.isExecuteBashActionEvent(t)&&N(t.action.command),o.isExecuteBashObservationEvent(t)&&P(t.observation.content.filter(e=>e.type===`text`).map(e=>e.text).join(`
3
3
  `)),o.isPlanningFileEditorObservationEvent(t)){let{path:e}=t.observation;if(ve(e)){let t=v?.[0]?.id;t&&e&&(I?U.current={path:e,conversationId:t}:V({conversationId:t,filePath:e},{onSuccess:e=>{B(e)},onError:e=>{console.warn(`Failed to read conversation file:`,e)}}))}}}}catch(e){console.warn(`Failed to parse WebSocket message as JSON:`,e)}},[E,I,R,O,j,T,v,h,M,N,P,V,B,G,W]),Te=(0,f.useMemo)(()=>{let e=J?{resend_mode:`since`,after_timestamp:J}:{resend_mode:`all`};return _&&(e.session_api_key=_),{queryParams:e,reconnect:{enabled:!0},onOpen:()=>{b(`OPEN`),C.current=!0,A()},onClose:()=>{b(`CLOSED`)},onError:()=>{b(`CLOSED`),C.current&&O(ae.SERVER_CONNECTION_ERROR_MESSAGE,`connection`)},onMessage:Ce}},[Ce,O,A,_,J]),Ee=(0,f.useMemo)(()=>{let e={resend_all:!0};_&&(e.session_api_key=_);let t=v?.[0];return{queryParams:e,reconnect:{enabled:!0},onOpen:async()=>{if(S(`OPEN`),w.current=!0,A(),t?.id&&t.conversation_url)try{let e=await de.default.getEventCount(t.id,t.conversation_url,t.session_api_key);z(e),e===0&&L(!1)}catch{L(!1)}},onClose:()=>{S(`CLOSED`)},onError:()=>{S(`CLOSED`),w.current&&O(ae.SERVER_CONNECTION_ERROR_MESSAGE,`connection`)},onMessage:we}},[we,O,A,_,v]),{socket:Q,reconnect:De}=ie.useWebSocket(Y||``,Te),{socket:$,reconnect:Oe}=ie.useWebSocket(X||``,Ee),ke=(0,f.useCallback)(()=>{if(k(),i.useConversationStore.getState().conversationMode===`plan`&&X){Oe();return}De()},[X,De,Oe,k]),Ae=(0,f.useCallback)(async e=>{let t=i.useConversationStore.getState().conversationMode===`plan`?$:Q;if(t?.readyState!==WebSocket.OPEN){if(!h){let e=Error(`No conversation ID available`);throw O(e.message),e}try{return await new te.ConversationClient(ne.getAgentServerClientOptions()).sendEvent(h,{role:`user`,content:e.content},{run:!0}),{queued:!0}}catch(e){throw O(e instanceof Error?e.message:`Failed to queue message for delivery`),e}}try{return t.send(JSON.stringify({...e,run:!0})),{queued:!1}}catch(e){throw O(e instanceof Error?e.message:`Failed to send message`),e}},[Q,$,O,h]);(0,f.useEffect)(()=>{Q&&Y&&(()=>{switch(Q.readyState){case WebSocket.CONNECTING:b(`CONNECTING`);break;case WebSocket.OPEN:b(`OPEN`);break;case WebSocket.CLOSING:b(`CLOSING`);break;case WebSocket.CLOSED:b(`CLOSED`);break;default:b(`CLOSED`);break}})()},[Q,Y]),(0,f.useEffect)(()=>{$&&X&&(()=>{switch($.readyState){case WebSocket.CONNECTING:S(`CONNECTING`);break;case WebSocket.OPEN:S(`OPEN`);break;case WebSocket.CLOSING:S(`CLOSING`);break;case WebSocket.CLOSED:S(`CLOSED`);break;default:S(`CLOSED`);break}})()},[$,X]);let je=(0,f.useMemo)(()=>({connectionState:Se,sendMessage:Ae,isLoadingHistory:Z,reconnect:ke}),[Se,Ae,Z,ke]);return(0,he.jsx)(p.Provider,{value:je,children:e})}var g=()=>(0,f.useContext)(p)||null;exports.ConversationWebSocketProvider=h,exports.useConversationWebSocket=g;
4
4
  //# sourceMappingURL=conversation-websocket-context.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-websocket-context.cjs","names":[],"sources":["../../src/contexts/conversation-websocket-context.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { ConversationClient } from \"@openhands/typescript-client/clients\";\n\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useWebSocket, WebSocketHookOptions } from \"#/hooks/use-websocket\";\nimport { SERVER_CONNECTION_ERROR_MESSAGE } from \"#/constants/server-connection-error\";\nimport { useEventStore } from \"#/stores/use-event-store\";\nimport { useErrorMessageStore } from \"#/stores/error-message-store\";\nimport { useOptimisticUserMessageStore } from \"#/stores/optimistic-user-message-store\";\nimport { useConversationStateStore } from \"#/stores/conversation-state-store\";\nimport { useCommandStore } from \"#/stores/command-store\";\nimport { useBrowserStore } from \"#/stores/browser-store\";\nimport { useGoalStore } from \"#/stores/goal-store\";\nimport {\n isAgentServerEvent,\n isAgentErrorEvent,\n isUserMessageEvent,\n isActionEvent,\n isConversationStateUpdateEvent,\n isFullStateConversationStateUpdateEvent,\n isAgentStatusConversationStateUpdateEvent,\n isStatsConversationStateUpdateEvent,\n isGoalConversationStateUpdateEvent,\n isExecuteBashActionEvent,\n isExecuteBashObservationEvent,\n isDisplayableErrorEvent,\n isPlanningFileEditorObservationEvent,\n isBrowserObservationEvent,\n isBrowserNavigateActionEvent,\n isSwitchLLMObservationEvent,\n isCanvasUIActionEvent,\n} from \"#/types/agent-server/type-guards\";\nimport { handleCanvasUIAction } from \"#/services/canvas-ui\";\nimport { ConversationStateUpdateEventStats } from \"#/types/agent-server/core/events/conversation-state-event\";\nimport type {\n ConversationErrorEvent,\n ServerErrorEvent,\n} from \"#/types/agent-server/core/events/conversation-state-event\";\nimport { handleActionEventCacheInvalidation } from \"#/utils/cache-utils\";\nimport { buildWebSocketUrl } from \"#/utils/websocket-url\";\nimport type {\n AppConversation,\n SendMessageRequest,\n} from \"#/api/conversation-service/agent-server-conversation-service.types\";\nimport EventService from \"#/api/event-service/event-service.api\";\nimport { getAgentServerClientOptions } from \"#/api/agent-server-client-options\";\nimport { useConversationStore } from \"#/stores/conversation-store\";\nimport { trackError } from \"#/utils/error-handler\";\nimport { useReadConversationFile } from \"#/hooks/mutation/use-read-conversation-file\";\nimport useMetricsStore from \"#/stores/metrics-store\";\nimport { useConversationHistory } from \"#/hooks/query/use-conversation-history\";\nimport { setConversationState } from \"#/utils/conversation-local-storage\";\nimport {\n recordModelSwitchMessage,\n seedModelSwitchesFromHistory,\n} from \"#/hooks/chat/record-model-switch-message\";\nimport {\n invalidateConversationQueries,\n updateConversationLlmModelInCache,\n} from \"#/hooks/mutation/conversation-mutation-utils\";\nimport {\n getStoredConversationMetadata,\n setStoredConversationMetadata,\n} from \"#/api/conversation-metadata-store\";\n\nexport type WebSocketConnectionState =\n | \"CONNECTING\"\n | \"OPEN\"\n | \"CLOSED\"\n | \"CLOSING\";\n\ninterface SendMessageResult {\n queued: boolean; // true if message was queued for later delivery, false if sent immediately\n}\n\ninterface ConversationWebSocketContextType {\n connectionState: WebSocketConnectionState;\n sendMessage: (message: SendMessageRequest) => Promise<SendMessageResult>;\n isLoadingHistory: boolean;\n reconnect: () => void;\n}\n\nconst ConversationWebSocketContext = createContext<\n ConversationWebSocketContextType | undefined\n>(undefined);\n\n/**\n * Extract the text body of an echoed user `MessageEvent` for matching against\n * the optimistic pending-message queue. The server wraps the original\n * `args.content` string in one or more `TextContent` entries (alongside any\n * `ImageContent` entries for inline images), so concatenating the `text`\n * fields gives us back the exact prompt we sent.\n */\nfunction extractMessageEventText(\n event: import(\"#/types/agent-server/core/events/message-event\").MessageEvent,\n): string {\n return event.llm_message.content\n .filter(\n (part): part is { type: \"text\"; text: string } => part.type === \"text\",\n )\n .map((part) => part.text)\n .join(\"\");\n}\n\nexport function ConversationWebSocketProvider({\n children,\n conversationId,\n conversationUrl,\n sessionApiKey,\n subConversations,\n subConversationIds,\n}: {\n children: React.ReactNode;\n conversationId?: string;\n conversationUrl?: string | null;\n sessionApiKey?: string | null;\n subConversations?: AppConversation[];\n subConversationIds?: string[];\n}) {\n // Separate connection state tracking for each WebSocket\n const [mainConnectionState, setMainConnectionState] =\n useState<WebSocketConnectionState>(\"CONNECTING\");\n const [planningConnectionState, setPlanningConnectionState] =\n useState<WebSocketConnectionState>(\"CONNECTING\");\n\n // Track if we've ever successfully connected for each connection\n // Don't show errors until after first successful connection\n const hasConnectedRefMain = React.useRef(false);\n const hasConnectedRefPlanning = React.useRef(false);\n\n const queryClient = useQueryClient();\n const addEvent = useEventStore((state) => state.addEvent);\n const addEvents = useEventStore((state) => state.addEvents);\n const clearEventsForConversation = useEventStore(\n (state) => state.clearEventsForConversation,\n );\n const { setErrorMessage, removeErrorMessage, clearConnectionError } =\n useErrorMessageStore();\n const consumeMatchingPendingMessage = useOptimisticUserMessageStore(\n (state) => state.consumeMatchingPendingMessage,\n );\n const { setExecutionStatus } = useConversationStateStore();\n const { appendInput, appendOutput } = useCommandStore();\n const resetBrowserStore = useBrowserStore((state) => state.reset);\n\n // History loading state.\n // - Main conversation history is now loaded via REST (`useConversationHistory`),\n // so its loading state mirrors the REST query state (see below).\n // - Planning sub-conversation history still streams over the WebSocket using\n // `resend_mode='all'`, so we keep the count-based detection for it.\n const [isLoadingHistoryPlanning, setIsLoadingHistoryPlanning] =\n useState(true);\n const [expectedEventCountPlanning, setExpectedEventCountPlanning] = useState<\n number | null\n >(null);\n\n const { setPlanContent } = useConversationStore();\n\n // Hook for reading conversation file\n const { mutate: readConversationFile } = useReadConversationFile();\n\n // Track planning-agent received events (still WS-driven).\n const receivedEventCountRefPlanning = useRef(0);\n\n // Track the latest PlanningFileEditorObservation for Plan.md during history replay\n const latestPlanningFileEventRef = useRef<{\n path: string;\n conversationId: string;\n } | null>(null);\n\n const isPlanFilePath = (path: string | null): boolean =>\n path?.toUpperCase().endsWith(\"PLAN.MD\") ?? false;\n\n const handleNonErrorEvent = useCallback(() => {\n // A normal event means connectivity recovered: clear a transient connection\n // error, but keep sticky conversation errors (e.g. a wrong API key).\n clearConnectionError();\n }, [clearConnectionError]);\n\n // Helper function to update metrics from stats event\n const updateMetricsFromStats = useCallback(\n (event: ConversationStateUpdateEventStats) => {\n if (event.value.usage_to_metrics?.agent) {\n const agentMetrics = event.value.usage_to_metrics.agent;\n const metrics = {\n cost: agentMetrics.accumulated_cost,\n max_budget_per_task: agentMetrics.max_budget_per_task ?? null,\n usage: agentMetrics.accumulated_token_usage\n ? {\n prompt_tokens:\n agentMetrics.accumulated_token_usage.prompt_tokens,\n completion_tokens:\n agentMetrics.accumulated_token_usage.completion_tokens,\n cache_read_tokens:\n agentMetrics.accumulated_token_usage.cache_read_tokens,\n cache_write_tokens:\n agentMetrics.accumulated_token_usage.cache_write_tokens,\n context_window:\n agentMetrics.accumulated_token_usage.context_window,\n per_turn_token:\n agentMetrics.accumulated_token_usage.per_turn_token,\n }\n : null,\n };\n useMetricsStore.getState().setMetrics(metrics);\n }\n },\n [],\n );\n\n // Initial REST history load: fetch the most recent events and seed the\n // store. Older events are paginated in via `useLoadOlderEvents` when the\n // user scrolls to the top of the chat. The WebSocket connection waits for\n // this query so it can subscribe with `resend_mode='since'` and avoid\n // re-streaming everything REST already returned.\n const {\n data: preloadedHistory,\n isPending: isPreloadingHistory,\n isFetching: isFetchingHistory,\n isError: isPreloadHistoryError,\n } = useConversationHistory(conversationId);\n\n // Skeleton only on the genuine first load (no cached data yet). On return the\n // cached page is present, so `isPending` is false and we render the\n // last-known discussion immediately while the tail refetch runs in the\n // background — see the socket gate below, which waits on `isFetching`.\n const isLoadingHistoryMain = !!conversationId && isPreloadingHistory;\n\n // Clear the (global, not conversation-scoped) event store when the active\n // conversation changes, BEFORE the preloaded-history effect below re-seeds\n // it. This MUST live here rather than in the route component: a parent's\n // passive effect runs *after* this child's layout effects, so clearing from\n // the route would wipe the freshly seeded history. On a conversation switch\n // the history page is already cached, so `preloadedHistory` is available\n // synchronously — without ordering the clear first, the user's already-echoed\n // message gets seeded then immediately wiped, leaving only the `since`\n // WebSocket resend (the agent's reply). Re-entering the same conversation is\n // a no-op, so the store survives navigating away to Settings and back.\n useLayoutEffect(() => {\n const nextId = conversationId ?? null;\n if (useEventStore.getState().loadedConversationId === nextId) {\n return;\n }\n // Single atomic action: clears the previous conversation's events and\n // records the new loaded id in one `set`, so no subscriber can observe a\n // half-applied state (events gone but the old id still reported).\n clearEventsForConversation(nextId);\n resetBrowserStore();\n }, [conversationId, clearEventsForConversation, resetBrowserStore]);\n\n useLayoutEffect(() => {\n if (!preloadedHistory || preloadedHistory.events.length === 0) {\n return;\n }\n addEvents(preloadedHistory.events);\n\n // The first user message of a cloud start-task conversation is persisted\n // server-side and reaches us via this REST preload, not over the WebSocket\n // (which subscribes with resend_mode='since' after the latest preloaded\n // timestamp). Consume any matching optimistic \"Sending…\" bubble here too —\n // mirroring the WS handler — so it doesn't linger as a duplicate of the echo.\n if (conversationId) {\n // Rebuild inline \"Switched to\" messages from the REST-preloaded history.\n // The live store writers (WS handler / user action) never see preloaded\n // events, so without this past model switches wouldn't render on reload.\n // Read the post-`addEvents` `uiEvents` (actions replaced by observations,\n // Think/Finish observations dropped) — not the raw history — so anchors\n // match the ids the renderer actually mounts.\n seedModelSwitchesFromHistory(\n conversationId,\n useEventStore.getState().uiEvents,\n );\n\n for (const event of preloadedHistory.events) {\n if (isUserMessageEvent(event)) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n }\n }\n }\n }, [\n preloadedHistory,\n addEvents,\n conversationId,\n consumeMatchingPendingMessage,\n ]);\n\n /**\n * Timestamp of the latest event we already have from REST. Used as\n * `after_timestamp` when opening the WebSocket so the server only resends\n * events strictly after this point. `null` until the REST query settles\n * (we hold the WS connection open until then to avoid an `all` resend).\n */\n const initialAfterTimestamp = useMemo<string | null>(() => {\n // Wait for the history query to settle — including the refetch fired when\n // returning to a conversation — so we anchor `since` to the freshest event\n // we have rather than a stale cached tail.\n if (isFetchingHistory) return null;\n const events = preloadedHistory?.events ?? [];\n const latest = events[events.length - 1];\n if (!latest || !(\"timestamp\" in latest) || !latest.timestamp) return null;\n return latest.timestamp;\n }, [preloadedHistory, isFetchingHistory]);\n\n // Build WebSocket URL from props.\n //\n // We deliberately wait for the history fetch to settle before opening the\n // socket so the WS subscription can use `resend_mode='since'` with a\n // meaningful `after_timestamp`. This gate is on `isFetching`, not just the\n // first-load `isPending`, so the refetch fired when returning to a\n // conversation also completes first: the socket bakes `after_timestamp` into\n // its URL at connect time and will NOT re-subscribe when the value changes\n // later (see use-websocket — options live in a ref, reconnect keys on the URL\n // only). Connecting mid-fetch would therefore pin `since` to the stale cached\n // tail and replay the entire backlog over the socket. Without any gate the WS\n // would instead fall back to `resend_mode='all'`. If the REST query errored we\n // fall through and connect with `resend_mode='all'` so the user still sees\n // live events.\n const wsUrl = useMemo(() => {\n if (!conversationId || !conversationUrl) {\n return null;\n }\n if (isFetchingHistory && !isPreloadHistoryError) {\n return null;\n }\n return buildWebSocketUrl(conversationId, conversationUrl);\n }, [\n conversationId,\n conversationUrl,\n isFetchingHistory,\n isPreloadHistoryError,\n ]);\n\n const planningAgentWsUrl = useMemo(() => {\n if (!subConversations?.length) {\n return null;\n }\n\n // Currently, there is only one sub-conversation and it uses the planning agent.\n const planningAgentConversation = subConversations[0];\n\n if (\n !planningAgentConversation?.id ||\n !planningAgentConversation.conversation_url\n ) {\n return null;\n }\n\n return buildWebSocketUrl(\n planningAgentConversation.id,\n planningAgentConversation.conversation_url,\n );\n }, [subConversations]);\n\n // Merged connection state - reflects combined status of both connections\n const connectionState = useMemo<WebSocketConnectionState>(() => {\n // If planning agent connection doesn't exist, use main connection state\n if (!planningAgentWsUrl) {\n return mainConnectionState;\n }\n\n // If either is connecting, merged state is connecting\n if (\n mainConnectionState === \"CONNECTING\" ||\n planningConnectionState === \"CONNECTING\"\n ) {\n return \"CONNECTING\";\n }\n\n // If both are open, merged state is open\n if (mainConnectionState === \"OPEN\" && planningConnectionState === \"OPEN\") {\n return \"OPEN\";\n }\n\n // If both are closed, merged state is closed\n if (\n mainConnectionState === \"CLOSED\" &&\n planningConnectionState === \"CLOSED\"\n ) {\n return \"CLOSED\";\n }\n\n // If either is closing, merged state is closing\n if (\n mainConnectionState === \"CLOSING\" ||\n planningConnectionState === \"CLOSING\"\n ) {\n return \"CLOSING\";\n }\n\n // Default to closed if states don't match expected patterns\n return \"CLOSED\";\n }, [mainConnectionState, planningConnectionState, planningAgentWsUrl]);\n\n useEffect(() => {\n if (\n expectedEventCountPlanning !== null &&\n receivedEventCountRefPlanning.current >= expectedEventCountPlanning &&\n isLoadingHistoryPlanning\n ) {\n setIsLoadingHistoryPlanning(false);\n }\n }, [\n expectedEventCountPlanning,\n isLoadingHistoryPlanning,\n receivedEventCountRefPlanning,\n ]);\n\n // Call API once after history loading completes if we tracked any PlanningFileEditorObservation events\n useEffect(() => {\n if (!isLoadingHistoryPlanning && latestPlanningFileEventRef.current) {\n const { path, conversationId: currentPlanningConversationId } =\n latestPlanningFileEventRef.current;\n\n readConversationFile(\n {\n conversationId: currentPlanningConversationId,\n filePath: path,\n },\n {\n onSuccess: (fileContent) => {\n setPlanContent(fileContent);\n },\n onError: (error) => {\n console.warn(\"Failed to read conversation file:\", error);\n },\n },\n );\n\n // Clear the ref after calling the API\n latestPlanningFileEventRef.current = null;\n }\n }, [isLoadingHistoryPlanning, readConversationFile, setPlanContent]);\n\n useEffect(() => {\n hasConnectedRefMain.current = false;\n setIsLoadingHistoryPlanning(!!subConversationIds?.length);\n setExpectedEventCountPlanning(null);\n receivedEventCountRefPlanning.current = 0;\n // Reset the tracked event ref when sub-conversations change\n latestPlanningFileEventRef.current = null;\n }, [subConversationIds]);\n\n // Reset hasConnected flags when the conversation changes.\n useEffect(() => {\n hasConnectedRefMain.current = false;\n hasConnectedRefPlanning.current = false;\n // Reset the tracked event ref when conversation changes\n latestPlanningFileEventRef.current = null;\n }, [conversationId]);\n\n // Merged loading history state - true if either connection is still loading\n const isLoadingHistory = useMemo(\n () => isLoadingHistoryMain || isLoadingHistoryPlanning,\n [isLoadingHistoryMain, isLoadingHistoryPlanning],\n );\n\n // Separate message handlers for each connection\n const handleMainMessage = useCallback(\n (messageEvent: MessageEvent) => {\n try {\n const event = JSON.parse(messageEvent.data);\n\n // History loading for the main conversation is REST-driven now;\n // every WS message is a new event we add to the store.\n\n // Use type guard to validate v1 event structure\n if (isAgentServerEvent(event)) {\n const isDuplicateEvent = useEventStore\n .getState()\n .eventIds.has(event.id);\n const switchLLMObservation =\n !isDuplicateEvent && isSwitchLLMObservationEvent(event)\n ? event\n : null;\n addEvent(event);\n\n // Handle displayable error events - show error banner\n // AgentErrorEvent errors are displayed inline in the chat, not as banners\n if (isDisplayableErrorEvent(event)) {\n const errorEvent = event as\n | ConversationErrorEvent\n | ServerErrorEvent;\n trackError({\n message: errorEvent.detail,\n source: \"conversation\",\n metadata: {\n eventId: errorEvent.id,\n errorCode: errorEvent.code,\n },\n });\n setErrorMessage(errorEvent.detail, \"conversation\", errorEvent.code);\n } else {\n handleNonErrorEvent();\n }\n\n // LLM errors render inline in the chat (see ErrorEventMessage); track\n // them for analytics but keep them out of the banner above the chat box.\n if (isAgentErrorEvent(event)) {\n trackError({\n message: event.error,\n source: \"agent\",\n metadata: {\n eventId: event.id,\n toolName: event.tool_name,\n toolCallId: event.tool_call_id,\n },\n });\n }\n\n // Clear optimistic user message when a user message is confirmed.\n // We match by the echoed text content (with FIFO fallback inside the\n // store), so an echo for \"second\" pops \"second\" — not whichever\n // pending entry happens to be oldest — protecting against any\n // out-of-order delivery between conversations or sub-agents.\n if (isUserMessageEvent(event)) {\n if (conversationId) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n // Clear draft from localStorage - message was successfully delivered\n setConversationState(conversationId, { draftMessage: null });\n }\n }\n\n // Handle cache invalidation for ActionEvent\n if (isActionEvent(event)) {\n const currentConversationId =\n conversationId || \"test-conversation-id\"; // TODO: Get from context\n handleActionEventCacheInvalidation(\n event,\n currentConversationId,\n queryClient,\n );\n }\n\n // Handle conversation state updates\n // TODO: Tests\n if (isConversationStateUpdateEvent(event)) {\n if (isFullStateConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value.execution_status);\n }\n if (isAgentStatusConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value);\n }\n if (isStatsConversationStateUpdateEvent(event)) {\n updateMetricsFromStats(event);\n }\n // Mirror goal status into the store. Intentionally duplicated across\n // the main and planning WebSocket handlers (like the execution_status\n // and stats branches above), not a merge artifact.\n if (isGoalConversationStateUpdateEvent(event) && conversationId) {\n useGoalStore.getState().setStatus(conversationId, event.value);\n }\n }\n\n // Handle ExecuteBashAction events - add command as input to terminal\n if (isExecuteBashActionEvent(event)) {\n appendInput(event.action.command);\n }\n\n // Handle ExecuteBashObservation events - add output to terminal\n if (isExecuteBashObservationEvent(event)) {\n // Extract text content from the observation content array\n const textContent = event.observation.content\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n appendOutput(textContent);\n }\n\n // Handle BrowserObservation events - update browser store with screenshot\n if (isBrowserObservationEvent(event)) {\n const { screenshot_data: screenshotData } = event.observation;\n if (screenshotData) {\n const screenshotSrc = screenshotData.startsWith(\"data:\")\n ? screenshotData\n : `data:image/png;base64,${screenshotData}`;\n useBrowserStore.getState().setScreenshotSrc(screenshotSrc);\n }\n }\n\n // Handle BrowserNavigateAction events - update browser store with URL\n if (isBrowserNavigateActionEvent(event)) {\n useBrowserStore.getState().setUrl(event.action.url);\n }\n\n if (\n conversationId &&\n switchLLMObservation &&\n !switchLLMObservation.observation.is_error\n ) {\n recordModelSwitchMessage(\n conversationId,\n switchLLMObservation.observation.profile_name,\n );\n\n // Mirror the user-driven `/model` path: persist the profile so the\n // chat-header switcher shows the right name after a reload, even\n // when several profiles share a model (#1082).\n const prevMetadata = getStoredConversationMetadata(conversationId);\n setStoredConversationMetadata(conversationId, {\n selected_repository: prevMetadata?.selected_repository ?? null,\n selected_branch: prevMetadata?.selected_branch ?? null,\n git_provider: prevMetadata?.git_provider ?? null,\n selected_workspace: prevMetadata?.selected_workspace ?? null,\n active_profile: switchLLMObservation.observation.profile_name,\n // Full-object replace: carry the plugins snapshot forward so the\n // in-conversation plugins view survives a profile switch.\n plugins: prevMetadata?.plugins ?? null,\n });\n\n if (switchLLMObservation.observation.active_model) {\n updateConversationLlmModelInCache(\n queryClient,\n conversationId,\n switchLLMObservation.observation.active_model,\n );\n }\n\n invalidateConversationQueries(queryClient, conversationId);\n }\n\n // Handle canvas_ui ActionEvents from both the legacy Python tool and\n // the client-defined JSON tool. The server acknowledges immediately;\n // the actual UI change happens here on the client.\n if (isCanvasUIActionEvent(event)) {\n handleCanvasUIAction(event.action, conversationId ?? null);\n }\n }\n } catch (error) {\n console.warn(\"Failed to parse WebSocket message as JSON:\", error);\n }\n },\n [\n addEvent,\n setErrorMessage,\n consumeMatchingPendingMessage,\n queryClient,\n conversationId,\n setExecutionStatus,\n appendInput,\n appendOutput,\n updateMetricsFromStats,\n handleNonErrorEvent,\n ],\n );\n\n const handlePlanningMessage = useCallback(\n (messageEvent: MessageEvent) => {\n try {\n const event = JSON.parse(messageEvent.data);\n\n // Track received events for history loading (count ALL events from WebSocket)\n // Always count when loading, even if we don't have the expected count yet\n if (isLoadingHistoryPlanning) {\n receivedEventCountRefPlanning.current += 1;\n\n if (\n expectedEventCountPlanning !== null &&\n receivedEventCountRefPlanning.current >= expectedEventCountPlanning\n ) {\n setIsLoadingHistoryPlanning(false);\n }\n }\n\n // Use type guard to validate v1 event structure\n if (isAgentServerEvent(event)) {\n // Mark this event as coming from the planning agent\n const eventWithPlanningFlag = {\n ...event,\n isFromPlanningAgent: true,\n };\n addEvent(eventWithPlanningFlag);\n\n // Handle displayable error events - show error banner\n // AgentErrorEvent errors are displayed inline in the chat, not as banners\n if (isDisplayableErrorEvent(event)) {\n const errorEvent = event as\n | ConversationErrorEvent\n | ServerErrorEvent;\n trackError({\n message: errorEvent.detail,\n source: \"planning_conversation\",\n metadata: {\n eventId: errorEvent.id,\n errorCode: errorEvent.code,\n },\n });\n setErrorMessage(errorEvent.detail, \"conversation\", errorEvent.code);\n } else {\n handleNonErrorEvent();\n }\n\n // LLM errors render inline in the chat (see ErrorEventMessage); track\n // them for analytics but keep them out of the banner above the chat box.\n if (isAgentErrorEvent(event)) {\n trackError({\n message: event.error,\n source: \"planning_agent\",\n metadata: {\n eventId: event.id,\n toolName: event.tool_name,\n toolCallId: event.tool_call_id,\n },\n });\n }\n\n // Clear optimistic user message when a user message is confirmed.\n // Always scope to the main `conversationId` (where the user types)\n // and match on the echoed content so the planning sub-agent's own\n // events can never consume a main-conversation pending entry.\n if (isUserMessageEvent(event)) {\n if (conversationId) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n setConversationState(conversationId, { draftMessage: null });\n }\n }\n\n // Handle cache invalidation for ActionEvent\n if (isActionEvent(event)) {\n const planningAgentConversation = subConversations?.[0];\n const currentConversationId =\n planningAgentConversation?.id || \"test-conversation-id\"; // TODO: Get from context\n handleActionEventCacheInvalidation(\n event,\n currentConversationId,\n queryClient,\n );\n }\n\n // Handle conversation state updates\n // TODO: Tests\n if (isConversationStateUpdateEvent(event)) {\n if (isFullStateConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value.execution_status);\n }\n if (isAgentStatusConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value);\n }\n if (isStatsConversationStateUpdateEvent(event)) {\n updateMetricsFromStats(event);\n }\n // Mirror goal status into the store. Intentionally duplicated across\n // the main and planning WebSocket handlers (like the execution_status\n // and stats branches above), not a merge artifact.\n if (isGoalConversationStateUpdateEvent(event) && conversationId) {\n useGoalStore.getState().setStatus(conversationId, event.value);\n }\n }\n\n // Handle ExecuteBashAction events - add command as input to terminal\n if (isExecuteBashActionEvent(event)) {\n appendInput(event.action.command);\n }\n\n // Handle ExecuteBashObservation events - add output to terminal\n if (isExecuteBashObservationEvent(event)) {\n // Extract text content from the observation content array\n const textContent = event.observation.content\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n appendOutput(textContent);\n }\n\n // Handle PlanningFileEditorObservation - only update plan for Plan.md\n if (isPlanningFileEditorObservationEvent(event)) {\n const { path } = event.observation;\n if (isPlanFilePath(path)) {\n const planningAgentConversation = subConversations?.[0];\n const planningConversationId = planningAgentConversation?.id;\n\n if (planningConversationId && path) {\n if (isLoadingHistoryPlanning) {\n latestPlanningFileEventRef.current = {\n path,\n conversationId: planningConversationId,\n };\n } else {\n readConversationFile(\n {\n conversationId: planningConversationId,\n filePath: path,\n },\n {\n onSuccess: (fileContent) => {\n setPlanContent(fileContent);\n },\n onError: (error) => {\n console.warn(\n \"Failed to read conversation file:\",\n error,\n );\n },\n },\n );\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn(\"Failed to parse WebSocket message as JSON:\", error);\n }\n },\n [\n addEvent,\n isLoadingHistoryPlanning,\n expectedEventCountPlanning,\n setErrorMessage,\n consumeMatchingPendingMessage,\n queryClient,\n subConversations,\n conversationId,\n setExecutionStatus,\n appendInput,\n appendOutput,\n readConversationFile,\n setPlanContent,\n updateMetricsFromStats,\n handleNonErrorEvent,\n ],\n );\n\n // Separate WebSocket options for main connection\n const mainWebsocketOptions: WebSocketHookOptions = useMemo(() => {\n // History was already loaded over REST (`useConversationHistory`).\n // Subscribe with `resend_mode='since'` so the server only resends events\n // strictly after the latest one we already have. If REST returned no\n // events at all (brand-new conversation), fall back to `'all'` so any\n // events that may have been written between the REST call and the WS\n // handshake still show up. Dedup in the event store handles overlap.\n const queryParams: Record<string, string | boolean> = initialAfterTimestamp\n ? { resend_mode: \"since\", after_timestamp: initialAfterTimestamp }\n : { resend_mode: \"all\" };\n\n // Add session_api_key if available\n if (sessionApiKey) {\n queryParams.session_api_key = sessionApiKey;\n }\n\n return {\n queryParams,\n reconnect: { enabled: true },\n onOpen: () => {\n setMainConnectionState(\"OPEN\");\n hasConnectedRefMain.current = true; // Mark that we've successfully connected\n clearConnectionError(); // Clear a previous connection error; keep sticky conversation errors\n },\n onClose: () => {\n setMainConnectionState(\"CLOSED\");\n },\n onError: () => {\n setMainConnectionState(\"CLOSED\");\n // Only show error message if we've previously connected successfully\n if (hasConnectedRefMain.current) {\n setErrorMessage(SERVER_CONNECTION_ERROR_MESSAGE, \"connection\");\n }\n },\n onMessage: handleMainMessage,\n };\n }, [\n handleMainMessage,\n setErrorMessage,\n clearConnectionError,\n sessionApiKey,\n initialAfterTimestamp,\n ]);\n\n // Separate WebSocket options for planning agent connection\n const planningWebsocketOptions: WebSocketHookOptions = useMemo(() => {\n const queryParams: Record<string, string | boolean> = {\n resend_all: true,\n };\n\n // Add session_api_key if available\n if (sessionApiKey) {\n queryParams.session_api_key = sessionApiKey;\n }\n\n const planningAgentConversation = subConversations?.[0];\n\n return {\n queryParams,\n reconnect: { enabled: true },\n onOpen: async () => {\n setPlanningConnectionState(\"OPEN\");\n hasConnectedRefPlanning.current = true; // Mark that we've successfully connected\n clearConnectionError(); // Clear a previous connection error; keep sticky conversation errors\n\n // Fetch expected event count for history loading detection\n if (\n planningAgentConversation?.id &&\n planningAgentConversation.conversation_url\n ) {\n try {\n const count = await EventService.getEventCount(\n planningAgentConversation.id,\n planningAgentConversation.conversation_url,\n planningAgentConversation.session_api_key,\n );\n setExpectedEventCountPlanning(count);\n\n // If no events expected, mark as loaded immediately\n if (count === 0) {\n setIsLoadingHistoryPlanning(false);\n }\n } catch (error) {\n // Fall back to marking as loaded to avoid infinite loading state\n setIsLoadingHistoryPlanning(false);\n }\n }\n },\n onClose: () => {\n setPlanningConnectionState(\"CLOSED\");\n },\n onError: () => {\n setPlanningConnectionState(\"CLOSED\");\n // Only show error message if we've previously connected successfully\n if (hasConnectedRefPlanning.current) {\n setErrorMessage(SERVER_CONNECTION_ERROR_MESSAGE, \"connection\");\n }\n },\n onMessage: handlePlanningMessage,\n };\n }, [\n handlePlanningMessage,\n setErrorMessage,\n clearConnectionError,\n sessionApiKey,\n subConversations,\n ]);\n\n // Only attempt WebSocket connection when we have a valid URL\n // This prevents connection attempts during task polling phase\n const websocketUrl = wsUrl;\n const { socket: mainSocket, reconnect: reconnectMain } = useWebSocket(\n websocketUrl || \"\",\n mainWebsocketOptions,\n );\n\n const { socket: planningAgentSocket, reconnect: reconnectPlanning } =\n useWebSocket(planningAgentWsUrl || \"\", planningWebsocketOptions);\n\n const reconnect = useCallback(() => {\n removeErrorMessage();\n const currentMode = useConversationStore.getState().conversationMode;\n if (currentMode === \"plan\" && planningAgentWsUrl) {\n reconnectPlanning();\n return;\n }\n reconnectMain();\n }, [\n planningAgentWsUrl,\n reconnectMain,\n reconnectPlanning,\n removeErrorMessage,\n ]);\n\n // V1 send message function via WebSocket\n // Falls back to REST API queue when WebSocket is not connected\n const sendMessage = useCallback(\n async (message: SendMessageRequest): Promise<SendMessageResult> => {\n const currentMode = useConversationStore.getState().conversationMode;\n const currentSocket =\n currentMode === \"plan\" ? planningAgentSocket : mainSocket;\n\n if (currentSocket?.readyState !== WebSocket.OPEN) {\n // WebSocket not connected - queue message via REST API\n // Message will be delivered automatically when conversation becomes ready\n if (!conversationId) {\n const error = new Error(\"No conversation ID available\");\n setErrorMessage(error.message);\n throw error;\n }\n\n try {\n await new ConversationClient(getAgentServerClientOptions()).sendEvent(\n conversationId,\n {\n role: \"user\",\n content: message.content,\n },\n { run: true },\n );\n // Message queued successfully - it will be delivered when ready\n // Return queued: true so caller knows not to show optimistic UI\n return { queued: true };\n } catch (error) {\n const errorMessage =\n error instanceof Error\n ? error.message\n : \"Failed to queue message for delivery\";\n setErrorMessage(errorMessage);\n throw error;\n }\n }\n\n try {\n // Send message through WebSocket as JSON with run: true so the\n // agent loop starts automatically in async mode.\n currentSocket.send(JSON.stringify({ ...message, run: true }));\n return { queued: false };\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : \"Failed to send message\";\n setErrorMessage(errorMessage);\n throw error;\n }\n },\n [mainSocket, planningAgentSocket, setErrorMessage, conversationId],\n );\n\n // Track main socket state changes\n useEffect(() => {\n // Only process socket updates if we have a valid URL and socket\n if (mainSocket && wsUrl) {\n // Update state based on socket readyState\n const updateState = () => {\n switch (mainSocket.readyState) {\n case WebSocket.CONNECTING:\n setMainConnectionState(\"CONNECTING\");\n break;\n case WebSocket.OPEN:\n setMainConnectionState(\"OPEN\");\n break;\n case WebSocket.CLOSING:\n setMainConnectionState(\"CLOSING\");\n break;\n case WebSocket.CLOSED:\n setMainConnectionState(\"CLOSED\");\n break;\n default:\n setMainConnectionState(\"CLOSED\");\n break;\n }\n };\n\n updateState();\n }\n }, [mainSocket, wsUrl]);\n\n // Track planning agent socket state changes\n useEffect(() => {\n // Only process socket updates if we have a valid URL and socket\n if (planningAgentSocket && planningAgentWsUrl) {\n // Update state based on socket readyState\n const updateState = () => {\n switch (planningAgentSocket.readyState) {\n case WebSocket.CONNECTING:\n setPlanningConnectionState(\"CONNECTING\");\n break;\n case WebSocket.OPEN:\n setPlanningConnectionState(\"OPEN\");\n break;\n case WebSocket.CLOSING:\n setPlanningConnectionState(\"CLOSING\");\n break;\n case WebSocket.CLOSED:\n setPlanningConnectionState(\"CLOSED\");\n break;\n default:\n setPlanningConnectionState(\"CLOSED\");\n break;\n }\n };\n\n updateState();\n }\n }, [planningAgentSocket, planningAgentWsUrl]);\n\n const contextValue = useMemo(\n () => ({ connectionState, sendMessage, isLoadingHistory, reconnect }),\n [connectionState, sendMessage, isLoadingHistory, reconnect],\n );\n\n return (\n <ConversationWebSocketContext.Provider value={contextValue}>\n {children}\n </ConversationWebSocketContext.Provider>\n );\n}\n\nexport const useConversationWebSocket =\n (): ConversationWebSocketContextType | null => {\n const context = useContext(ConversationWebSocketContext);\n // Return null instead of throwing when not in provider\n // This allows the hook to be called conditionally based on conversation version\n return context || null;\n };\n"],"mappings":"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"}
1
+ {"version":3,"file":"conversation-websocket-context.cjs","names":[],"sources":["../../src/contexts/conversation-websocket-context.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n useCallback,\n useMemo,\n useRef,\n} from \"react\";\nimport { ConversationClient } from \"@openhands/typescript-client/clients\";\n\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useWebSocket, WebSocketHookOptions } from \"#/hooks/use-websocket\";\nimport { SERVER_CONNECTION_ERROR_MESSAGE } from \"#/constants/server-connection-error\";\nimport { useEventStore } from \"#/stores/use-event-store\";\nimport { useErrorMessageStore } from \"#/stores/error-message-store\";\nimport { useOptimisticUserMessageStore } from \"#/stores/optimistic-user-message-store\";\nimport { useConversationStateStore } from \"#/stores/conversation-state-store\";\nimport { useCommandStore } from \"#/stores/command-store\";\nimport { useBrowserStore } from \"#/stores/browser-store\";\nimport { useGoalStore } from \"#/stores/goal-store\";\nimport {\n isAgentServerEvent,\n isAgentErrorEvent,\n isUserMessageEvent,\n isActionEvent,\n isConversationStateUpdateEvent,\n isFullStateConversationStateUpdateEvent,\n isAgentStatusConversationStateUpdateEvent,\n isStatsConversationStateUpdateEvent,\n isGoalConversationStateUpdateEvent,\n isExecuteBashActionEvent,\n isExecuteBashObservationEvent,\n isDisplayableErrorEvent,\n isPlanningFileEditorObservationEvent,\n isBrowserObservationEvent,\n isBrowserNavigateActionEvent,\n isSwitchLLMObservationEvent,\n isCanvasUIActionEvent,\n} from \"#/types/agent-server/type-guards\";\nimport { handleCanvasUIAction } from \"#/services/canvas-ui\";\nimport { ConversationStateUpdateEventStats } from \"#/types/agent-server/core/events/conversation-state-event\";\nimport type {\n ConversationErrorEvent,\n ServerErrorEvent,\n} from \"#/types/agent-server/core/events/conversation-state-event\";\nimport { handleActionEventCacheInvalidation } from \"#/utils/cache-utils\";\nimport { buildWebSocketUrl } from \"#/utils/websocket-url\";\nimport type {\n AppConversation,\n SendMessageRequest,\n} from \"#/api/conversation-service/agent-server-conversation-service.types\";\nimport EventService from \"#/api/event-service/event-service.api\";\nimport { getAgentServerClientOptions } from \"#/api/agent-server-client-options\";\nimport { useConversationStore } from \"#/stores/conversation-store\";\nimport { trackError } from \"#/utils/error-handler\";\nimport { useReadConversationFile } from \"#/hooks/mutation/use-read-conversation-file\";\nimport useMetricsStore from \"#/stores/metrics-store\";\nimport { useConversationHistory } from \"#/hooks/query/use-conversation-history\";\nimport { setConversationState } from \"#/utils/conversation-local-storage\";\nimport {\n recordModelSwitchMessage,\n seedModelSwitchesFromHistory,\n} from \"#/hooks/chat/record-model-switch-message\";\nimport {\n invalidateConversationQueries,\n updateConversationLlmModelInCache,\n} from \"#/hooks/mutation/conversation-mutation-utils\";\nimport {\n getStoredConversationMetadata,\n setStoredConversationMetadata,\n} from \"#/api/conversation-metadata-store\";\n\nexport type WebSocketConnectionState =\n | \"CONNECTING\"\n | \"OPEN\"\n | \"CLOSED\"\n | \"CLOSING\";\n\ninterface SendMessageResult {\n queued: boolean; // true if message was queued for later delivery, false if sent immediately\n}\n\ninterface ConversationWebSocketContextType {\n connectionState: WebSocketConnectionState;\n sendMessage: (message: SendMessageRequest) => Promise<SendMessageResult>;\n isLoadingHistory: boolean;\n reconnect: () => void;\n}\n\nconst ConversationWebSocketContext = createContext<\n ConversationWebSocketContextType | undefined\n>(undefined);\n\n/**\n * Extract the text body of an echoed user `MessageEvent` for matching against\n * the optimistic pending-message queue. The server wraps the original\n * `args.content` string in one or more `TextContent` entries (alongside any\n * `ImageContent` entries for inline images), so concatenating the `text`\n * fields gives us back the exact prompt we sent.\n */\nfunction extractMessageEventText(\n event: import(\"#/types/agent-server/core/events/message-event\").MessageEvent,\n): string {\n return event.llm_message.content\n .filter(\n (part): part is { type: \"text\"; text: string } => part.type === \"text\",\n )\n .map((part) => part.text)\n .join(\"\");\n}\n\nexport function ConversationWebSocketProvider({\n children,\n conversationId,\n conversationUrl,\n sessionApiKey,\n subConversations,\n subConversationIds,\n}: {\n children: React.ReactNode;\n conversationId?: string;\n conversationUrl?: string | null;\n sessionApiKey?: string | null;\n subConversations?: AppConversation[];\n subConversationIds?: string[];\n}) {\n // Separate connection state tracking for each WebSocket\n const [mainConnectionState, setMainConnectionState] =\n useState<WebSocketConnectionState>(\"CONNECTING\");\n const [planningConnectionState, setPlanningConnectionState] =\n useState<WebSocketConnectionState>(\"CONNECTING\");\n\n // Track if we've ever successfully connected for each connection\n // Don't show errors until after first successful connection\n const hasConnectedRefMain = React.useRef(false);\n const hasConnectedRefPlanning = React.useRef(false);\n\n const queryClient = useQueryClient();\n const addEvent = useEventStore((state) => state.addEvent);\n const addEvents = useEventStore((state) => state.addEvents);\n const clearEventsForConversation = useEventStore(\n (state) => state.clearEventsForConversation,\n );\n const { setErrorMessage, removeErrorMessage, clearConnectionError } =\n useErrorMessageStore();\n const consumeMatchingPendingMessage = useOptimisticUserMessageStore(\n (state) => state.consumeMatchingPendingMessage,\n );\n const { setExecutionStatus } = useConversationStateStore();\n const { appendInput, appendOutput } = useCommandStore();\n const resetBrowserStore = useBrowserStore((state) => state.reset);\n\n // History loading state.\n // - Main conversation history is now loaded via REST (`useConversationHistory`),\n // so its loading state mirrors the REST query state (see below).\n // - Planning sub-conversation history still streams over the WebSocket using\n // `resend_mode='all'`, so we keep the count-based detection for it.\n const [isLoadingHistoryPlanning, setIsLoadingHistoryPlanning] =\n useState(true);\n const [expectedEventCountPlanning, setExpectedEventCountPlanning] = useState<\n number | null\n >(null);\n\n const { setPlanContent } = useConversationStore();\n\n // Hook for reading conversation file\n const { mutate: readConversationFile } = useReadConversationFile();\n\n // Track planning-agent received events (still WS-driven).\n const receivedEventCountRefPlanning = useRef(0);\n\n // Track the latest PlanningFileEditorObservation for Plan.md during history replay\n const latestPlanningFileEventRef = useRef<{\n path: string;\n conversationId: string;\n } | null>(null);\n\n const isPlanFilePath = (path: string | null): boolean =>\n path?.toUpperCase().endsWith(\"PLAN.MD\") ?? false;\n\n const handleNonErrorEvent = useCallback(() => {\n // A normal event means connectivity recovered: clear a transient connection\n // error, but keep sticky conversation errors (e.g. a wrong API key).\n clearConnectionError();\n }, [clearConnectionError]);\n\n // Helper function to update metrics from stats event\n const updateMetricsFromStats = useCallback(\n (event: ConversationStateUpdateEventStats) => {\n if (event.value.usage_to_metrics?.agent) {\n const agentMetrics = event.value.usage_to_metrics.agent;\n const metrics = {\n cost: agentMetrics.accumulated_cost,\n max_budget_per_task: agentMetrics.max_budget_per_task ?? null,\n usage: agentMetrics.accumulated_token_usage\n ? {\n prompt_tokens:\n agentMetrics.accumulated_token_usage.prompt_tokens,\n completion_tokens:\n agentMetrics.accumulated_token_usage.completion_tokens,\n cache_read_tokens:\n agentMetrics.accumulated_token_usage.cache_read_tokens,\n cache_write_tokens:\n agentMetrics.accumulated_token_usage.cache_write_tokens,\n context_window:\n agentMetrics.accumulated_token_usage.context_window,\n per_turn_token:\n agentMetrics.accumulated_token_usage.per_turn_token,\n }\n : null,\n };\n useMetricsStore.getState().setMetrics(metrics);\n }\n },\n [],\n );\n\n // Initial REST history load: fetch the most recent events and seed the\n // store. Older events are paginated in via `useLoadOlderEvents` when the\n // user scrolls to the top of the chat. The WebSocket connection waits for\n // this query so it can subscribe with `resend_mode='since'` and avoid\n // re-streaming everything REST already returned.\n const {\n data: preloadedHistory,\n isPending: isPreloadingHistory,\n isFetching: isFetchingHistory,\n isError: isPreloadHistoryError,\n } = useConversationHistory(conversationId);\n\n // Skeleton only on the genuine first load (no cached data yet). On return the\n // cached page is present, so `isPending` is false and we render the\n // last-known discussion immediately while the tail refetch runs in the\n // background — see the socket gate below, which waits on `isFetching`.\n const isLoadingHistoryMain = !!conversationId && isPreloadingHistory;\n\n // Clear the (global, not conversation-scoped) event store when the active\n // conversation changes, BEFORE the preloaded-history effect below re-seeds\n // it. This MUST live here rather than in the route component: a parent's\n // passive effect runs *after* this child's layout effects, so clearing from\n // the route would wipe the freshly seeded history. On a conversation switch\n // the history page is already cached, so `preloadedHistory` is available\n // synchronously — without ordering the clear first, the user's already-echoed\n // message gets seeded then immediately wiped, leaving only the `since`\n // WebSocket resend (the agent's reply). Re-entering the same conversation is\n // a no-op, so the store survives navigating away to Settings and back.\n useLayoutEffect(() => {\n const nextId = conversationId ?? null;\n if (useEventStore.getState().loadedConversationId === nextId) {\n return;\n }\n // Single atomic action: clears the previous conversation's events and\n // records the new loaded id in one `set`, so no subscriber can observe a\n // half-applied state (events gone but the old id still reported).\n clearEventsForConversation(nextId);\n resetBrowserStore();\n }, [conversationId, clearEventsForConversation, resetBrowserStore]);\n\n useLayoutEffect(() => {\n if (!preloadedHistory || preloadedHistory.events.length === 0) {\n return;\n }\n addEvents(preloadedHistory.events);\n\n // The first user message of a cloud start-task conversation is persisted\n // server-side and reaches us via this REST preload, not over the WebSocket\n // (which subscribes with resend_mode='since' after the latest preloaded\n // timestamp). Consume any matching optimistic \"Sending…\" bubble here too —\n // mirroring the WS handler — so it doesn't linger as a duplicate of the echo.\n if (conversationId) {\n // Rebuild inline \"Switched to\" messages from the REST-preloaded history.\n // The live store writers (WS handler / user action) never see preloaded\n // events, so without this past model switches wouldn't render on reload.\n // Read the post-`addEvents` `uiEvents` (actions replaced by observations,\n // Think/Finish observations dropped) — not the raw history — so anchors\n // match the ids the renderer actually mounts.\n seedModelSwitchesFromHistory(\n conversationId,\n useEventStore.getState().uiEvents,\n );\n\n for (const event of preloadedHistory.events) {\n if (isUserMessageEvent(event)) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n }\n }\n }\n }, [\n preloadedHistory,\n addEvents,\n conversationId,\n consumeMatchingPendingMessage,\n ]);\n\n /**\n * Timestamp of the latest event we already have from REST. Used as\n * `after_timestamp` when opening the WebSocket so the server only resends\n * events strictly after this point. `null` until the REST query settles\n * (we hold the WS connection open until then to avoid an `all` resend).\n */\n const initialAfterTimestamp = useMemo<string | null>(() => {\n // Wait for the history query to settle — including the refetch fired when\n // returning to a conversation — so we anchor `since` to the freshest event\n // we have rather than a stale cached tail.\n if (isFetchingHistory) return null;\n const events = preloadedHistory?.events ?? [];\n const latest = events[events.length - 1];\n if (!latest || !(\"timestamp\" in latest) || !latest.timestamp) return null;\n return latest.timestamp;\n }, [preloadedHistory, isFetchingHistory]);\n\n // Build WebSocket URL from props.\n //\n // We deliberately wait for the history fetch to settle before opening the\n // socket so the WS subscription can use `resend_mode='since'` with a\n // meaningful `after_timestamp`. This gate is on `isFetching`, not just the\n // first-load `isPending`, so the refetch fired when returning to a\n // conversation also completes first: the socket bakes `after_timestamp` into\n // its URL at connect time and will NOT re-subscribe when the value changes\n // later (see use-websocket — options live in a ref, reconnect keys on the URL\n // only). Connecting mid-fetch would therefore pin `since` to the stale cached\n // tail and replay the entire backlog over the socket. Without any gate the WS\n // would instead fall back to `resend_mode='all'`. If the REST query errored we\n // fall through and connect with `resend_mode='all'` so the user still sees\n // live events.\n const wsUrl = useMemo(() => {\n if (!conversationId || !conversationUrl) {\n return null;\n }\n if (isFetchingHistory && !isPreloadHistoryError) {\n return null;\n }\n return buildWebSocketUrl(conversationId, conversationUrl);\n }, [\n conversationId,\n conversationUrl,\n isFetchingHistory,\n isPreloadHistoryError,\n ]);\n\n const planningAgentWsUrl = useMemo(() => {\n if (!subConversations?.length) {\n return null;\n }\n\n // Currently, there is only one sub-conversation and it uses the planning agent.\n const planningAgentConversation = subConversations[0];\n\n if (\n !planningAgentConversation?.id ||\n !planningAgentConversation.conversation_url\n ) {\n return null;\n }\n\n return buildWebSocketUrl(\n planningAgentConversation.id,\n planningAgentConversation.conversation_url,\n );\n }, [subConversations]);\n\n // Merged connection state - reflects combined status of both connections\n const connectionState = useMemo<WebSocketConnectionState>(() => {\n // If planning agent connection doesn't exist, use main connection state\n if (!planningAgentWsUrl) {\n return mainConnectionState;\n }\n\n // If either is connecting, merged state is connecting\n if (\n mainConnectionState === \"CONNECTING\" ||\n planningConnectionState === \"CONNECTING\"\n ) {\n return \"CONNECTING\";\n }\n\n // If both are open, merged state is open\n if (mainConnectionState === \"OPEN\" && planningConnectionState === \"OPEN\") {\n return \"OPEN\";\n }\n\n // If both are closed, merged state is closed\n if (\n mainConnectionState === \"CLOSED\" &&\n planningConnectionState === \"CLOSED\"\n ) {\n return \"CLOSED\";\n }\n\n // If either is closing, merged state is closing\n if (\n mainConnectionState === \"CLOSING\" ||\n planningConnectionState === \"CLOSING\"\n ) {\n return \"CLOSING\";\n }\n\n // Default to closed if states don't match expected patterns\n return \"CLOSED\";\n }, [mainConnectionState, planningConnectionState, planningAgentWsUrl]);\n\n useEffect(() => {\n if (\n expectedEventCountPlanning !== null &&\n receivedEventCountRefPlanning.current >= expectedEventCountPlanning &&\n isLoadingHistoryPlanning\n ) {\n setIsLoadingHistoryPlanning(false);\n }\n }, [\n expectedEventCountPlanning,\n isLoadingHistoryPlanning,\n receivedEventCountRefPlanning,\n ]);\n\n // Call API once after history loading completes if we tracked any PlanningFileEditorObservation events\n useEffect(() => {\n if (!isLoadingHistoryPlanning && latestPlanningFileEventRef.current) {\n const { path, conversationId: currentPlanningConversationId } =\n latestPlanningFileEventRef.current;\n\n readConversationFile(\n {\n conversationId: currentPlanningConversationId,\n filePath: path,\n },\n {\n onSuccess: (fileContent) => {\n setPlanContent(fileContent);\n },\n onError: (error) => {\n console.warn(\"Failed to read conversation file:\", error);\n },\n },\n );\n\n // Clear the ref after calling the API\n latestPlanningFileEventRef.current = null;\n }\n }, [isLoadingHistoryPlanning, readConversationFile, setPlanContent]);\n\n useEffect(() => {\n hasConnectedRefMain.current = false;\n setIsLoadingHistoryPlanning(!!subConversationIds?.length);\n setExpectedEventCountPlanning(null);\n receivedEventCountRefPlanning.current = 0;\n // Reset the tracked event ref when sub-conversations change\n latestPlanningFileEventRef.current = null;\n }, [subConversationIds]);\n\n // Reset hasConnected flags when the conversation changes.\n useEffect(() => {\n hasConnectedRefMain.current = false;\n hasConnectedRefPlanning.current = false;\n // Reset the tracked event ref when conversation changes\n latestPlanningFileEventRef.current = null;\n }, [conversationId]);\n\n // Merged loading history state - true if either connection is still loading\n const isLoadingHistory = useMemo(\n () => isLoadingHistoryMain || isLoadingHistoryPlanning,\n [isLoadingHistoryMain, isLoadingHistoryPlanning],\n );\n\n // Separate message handlers for each connection\n const handleMainMessage = useCallback(\n (messageEvent: MessageEvent) => {\n try {\n const event = JSON.parse(messageEvent.data);\n\n // History loading for the main conversation is REST-driven now;\n // every WS message is a new event we add to the store.\n\n // Use type guard to validate v1 event structure\n if (isAgentServerEvent(event)) {\n // A reconnect replays the backlog from a stale anchor. The store\n // dedups by id, but the side-effects below aren't idempotent, so skip\n // them for replayed events (#1656).\n const isDuplicateEvent = useEventStore\n .getState()\n .eventIds.has(event.id);\n const switchLLMObservation = isSwitchLLMObservationEvent(event)\n ? event\n : null;\n addEvent(event);\n if (isDuplicateEvent) {\n return;\n }\n\n // Handle displayable error events - show error banner\n // AgentErrorEvent errors are displayed inline in the chat, not as banners\n if (isDisplayableErrorEvent(event)) {\n const errorEvent = event as\n | ConversationErrorEvent\n | ServerErrorEvent;\n trackError({\n message: errorEvent.detail,\n source: \"conversation\",\n metadata: {\n eventId: errorEvent.id,\n errorCode: errorEvent.code,\n },\n });\n setErrorMessage(errorEvent.detail, \"conversation\", errorEvent.code);\n } else {\n handleNonErrorEvent();\n }\n\n // LLM errors render inline in the chat (see ErrorEventMessage); track\n // them for analytics but keep them out of the banner above the chat box.\n if (isAgentErrorEvent(event)) {\n trackError({\n message: event.error,\n source: \"agent\",\n metadata: {\n eventId: event.id,\n toolName: event.tool_name,\n toolCallId: event.tool_call_id,\n },\n });\n }\n\n // Clear optimistic user message when a user message is confirmed.\n // We match by the echoed text content (with FIFO fallback inside the\n // store), so an echo for \"second\" pops \"second\" — not whichever\n // pending entry happens to be oldest — protecting against any\n // out-of-order delivery between conversations or sub-agents.\n if (isUserMessageEvent(event)) {\n if (conversationId) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n // Clear draft from localStorage - message was successfully delivered\n setConversationState(conversationId, { draftMessage: null });\n }\n }\n\n // Handle cache invalidation for ActionEvent\n if (isActionEvent(event)) {\n const currentConversationId =\n conversationId || \"test-conversation-id\"; // TODO: Get from context\n handleActionEventCacheInvalidation(\n event,\n currentConversationId,\n queryClient,\n );\n }\n\n // Handle conversation state updates\n // TODO: Tests\n if (isConversationStateUpdateEvent(event)) {\n if (isFullStateConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value.execution_status);\n }\n if (isAgentStatusConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value);\n }\n if (isStatsConversationStateUpdateEvent(event)) {\n updateMetricsFromStats(event);\n }\n // Mirror goal status into the store. Intentionally duplicated across\n // the main and planning WebSocket handlers (like the execution_status\n // and stats branches above), not a merge artifact.\n if (isGoalConversationStateUpdateEvent(event) && conversationId) {\n useGoalStore.getState().setStatus(conversationId, event.value);\n }\n }\n\n // Handle ExecuteBashAction events - add command as input to terminal\n if (isExecuteBashActionEvent(event)) {\n appendInput(event.action.command);\n }\n\n // Handle ExecuteBashObservation events - add output to terminal\n if (isExecuteBashObservationEvent(event)) {\n // Extract text content from the observation content array\n const textContent = event.observation.content\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n appendOutput(textContent);\n }\n\n // Handle BrowserObservation events - update browser store with screenshot\n if (isBrowserObservationEvent(event)) {\n const { screenshot_data: screenshotData } = event.observation;\n if (screenshotData) {\n const screenshotSrc = screenshotData.startsWith(\"data:\")\n ? screenshotData\n : `data:image/png;base64,${screenshotData}`;\n useBrowserStore.getState().setScreenshotSrc(screenshotSrc);\n }\n }\n\n // Handle BrowserNavigateAction events - update browser store with URL\n if (isBrowserNavigateActionEvent(event)) {\n useBrowserStore.getState().setUrl(event.action.url);\n }\n\n if (\n conversationId &&\n switchLLMObservation &&\n !switchLLMObservation.observation.is_error\n ) {\n recordModelSwitchMessage(\n conversationId,\n switchLLMObservation.observation.profile_name,\n );\n\n // Mirror the user-driven `/model` path: persist the profile so the\n // chat-header switcher shows the right name after a reload, even\n // when several profiles share a model (#1082).\n const prevMetadata = getStoredConversationMetadata(conversationId);\n setStoredConversationMetadata(conversationId, {\n selected_repository: prevMetadata?.selected_repository ?? null,\n selected_branch: prevMetadata?.selected_branch ?? null,\n git_provider: prevMetadata?.git_provider ?? null,\n selected_workspace: prevMetadata?.selected_workspace ?? null,\n active_profile: switchLLMObservation.observation.profile_name,\n // Full-object replace: carry the plugins snapshot forward so the\n // in-conversation plugins view survives a profile switch.\n plugins: prevMetadata?.plugins ?? null,\n });\n\n if (switchLLMObservation.observation.active_model) {\n updateConversationLlmModelInCache(\n queryClient,\n conversationId,\n switchLLMObservation.observation.active_model,\n );\n }\n\n invalidateConversationQueries(queryClient, conversationId);\n }\n\n // Handle canvas_ui ActionEvents from both the legacy Python tool and\n // the client-defined JSON tool. The server acknowledges immediately;\n // the actual UI change happens here on the client.\n if (isCanvasUIActionEvent(event)) {\n handleCanvasUIAction(event.action, conversationId ?? null);\n }\n }\n } catch (error) {\n console.warn(\"Failed to parse WebSocket message as JSON:\", error);\n }\n },\n [\n addEvent,\n setErrorMessage,\n consumeMatchingPendingMessage,\n queryClient,\n conversationId,\n setExecutionStatus,\n appendInput,\n appendOutput,\n updateMetricsFromStats,\n handleNonErrorEvent,\n ],\n );\n\n const handlePlanningMessage = useCallback(\n (messageEvent: MessageEvent) => {\n try {\n const event = JSON.parse(messageEvent.data);\n\n // Track received events for history loading (count ALL events from WebSocket)\n // Always count when loading, even if we don't have the expected count yet\n if (isLoadingHistoryPlanning) {\n receivedEventCountRefPlanning.current += 1;\n\n if (\n expectedEventCountPlanning !== null &&\n receivedEventCountRefPlanning.current >= expectedEventCountPlanning\n ) {\n setIsLoadingHistoryPlanning(false);\n }\n }\n\n // Use type guard to validate v1 event structure\n if (isAgentServerEvent(event)) {\n // Skip non-idempotent side-effects for replayed events, as in the\n // main handler (#1656).\n const isDuplicateEvent = useEventStore\n .getState()\n .eventIds.has(event.id);\n // Mark this event as coming from the planning agent\n const eventWithPlanningFlag = {\n ...event,\n isFromPlanningAgent: true,\n };\n addEvent(eventWithPlanningFlag);\n if (isDuplicateEvent) {\n return;\n }\n\n // Handle displayable error events - show error banner\n // AgentErrorEvent errors are displayed inline in the chat, not as banners\n if (isDisplayableErrorEvent(event)) {\n const errorEvent = event as\n | ConversationErrorEvent\n | ServerErrorEvent;\n trackError({\n message: errorEvent.detail,\n source: \"planning_conversation\",\n metadata: {\n eventId: errorEvent.id,\n errorCode: errorEvent.code,\n },\n });\n setErrorMessage(errorEvent.detail, \"conversation\", errorEvent.code);\n } else {\n handleNonErrorEvent();\n }\n\n // LLM errors render inline in the chat (see ErrorEventMessage); track\n // them for analytics but keep them out of the banner above the chat box.\n if (isAgentErrorEvent(event)) {\n trackError({\n message: event.error,\n source: \"planning_agent\",\n metadata: {\n eventId: event.id,\n toolName: event.tool_name,\n toolCallId: event.tool_call_id,\n },\n });\n }\n\n // Clear optimistic user message when a user message is confirmed.\n // Always scope to the main `conversationId` (where the user types)\n // and match on the echoed content so the planning sub-agent's own\n // events can never consume a main-conversation pending entry.\n if (isUserMessageEvent(event)) {\n if (conversationId) {\n consumeMatchingPendingMessage(\n conversationId,\n extractMessageEventText(event),\n );\n setConversationState(conversationId, { draftMessage: null });\n }\n }\n\n // Handle cache invalidation for ActionEvent\n if (isActionEvent(event)) {\n const planningAgentConversation = subConversations?.[0];\n const currentConversationId =\n planningAgentConversation?.id || \"test-conversation-id\"; // TODO: Get from context\n handleActionEventCacheInvalidation(\n event,\n currentConversationId,\n queryClient,\n );\n }\n\n // Handle conversation state updates\n // TODO: Tests\n if (isConversationStateUpdateEvent(event)) {\n if (isFullStateConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value.execution_status);\n }\n if (isAgentStatusConversationStateUpdateEvent(event)) {\n setExecutionStatus(event.value);\n }\n if (isStatsConversationStateUpdateEvent(event)) {\n updateMetricsFromStats(event);\n }\n // Mirror goal status into the store. Intentionally duplicated across\n // the main and planning WebSocket handlers (like the execution_status\n // and stats branches above), not a merge artifact.\n if (isGoalConversationStateUpdateEvent(event) && conversationId) {\n useGoalStore.getState().setStatus(conversationId, event.value);\n }\n }\n\n // Handle ExecuteBashAction events - add command as input to terminal\n if (isExecuteBashActionEvent(event)) {\n appendInput(event.action.command);\n }\n\n // Handle ExecuteBashObservation events - add output to terminal\n if (isExecuteBashObservationEvent(event)) {\n // Extract text content from the observation content array\n const textContent = event.observation.content\n .filter((c) => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n appendOutput(textContent);\n }\n\n // Handle PlanningFileEditorObservation - only update plan for Plan.md\n if (isPlanningFileEditorObservationEvent(event)) {\n const { path } = event.observation;\n if (isPlanFilePath(path)) {\n const planningAgentConversation = subConversations?.[0];\n const planningConversationId = planningAgentConversation?.id;\n\n if (planningConversationId && path) {\n if (isLoadingHistoryPlanning) {\n latestPlanningFileEventRef.current = {\n path,\n conversationId: planningConversationId,\n };\n } else {\n readConversationFile(\n {\n conversationId: planningConversationId,\n filePath: path,\n },\n {\n onSuccess: (fileContent) => {\n setPlanContent(fileContent);\n },\n onError: (error) => {\n console.warn(\n \"Failed to read conversation file:\",\n error,\n );\n },\n },\n );\n }\n }\n }\n }\n }\n } catch (error) {\n console.warn(\"Failed to parse WebSocket message as JSON:\", error);\n }\n },\n [\n addEvent,\n isLoadingHistoryPlanning,\n expectedEventCountPlanning,\n setErrorMessage,\n consumeMatchingPendingMessage,\n queryClient,\n subConversations,\n conversationId,\n setExecutionStatus,\n appendInput,\n appendOutput,\n readConversationFile,\n setPlanContent,\n updateMetricsFromStats,\n handleNonErrorEvent,\n ],\n );\n\n // Separate WebSocket options for main connection\n const mainWebsocketOptions: WebSocketHookOptions = useMemo(() => {\n // History was already loaded over REST (`useConversationHistory`).\n // Subscribe with `resend_mode='since'` so the server only resends events\n // strictly after the latest one we already have. If REST returned no\n // events at all (brand-new conversation), fall back to `'all'` so any\n // events that may have been written between the REST call and the WS\n // handshake still show up. Dedup in the event store handles overlap.\n const queryParams: Record<string, string | boolean> = initialAfterTimestamp\n ? { resend_mode: \"since\", after_timestamp: initialAfterTimestamp }\n : { resend_mode: \"all\" };\n\n // Add session_api_key if available\n if (sessionApiKey) {\n queryParams.session_api_key = sessionApiKey;\n }\n\n return {\n queryParams,\n reconnect: { enabled: true },\n onOpen: () => {\n setMainConnectionState(\"OPEN\");\n hasConnectedRefMain.current = true; // Mark that we've successfully connected\n clearConnectionError(); // Clear a previous connection error; keep sticky conversation errors\n },\n onClose: () => {\n setMainConnectionState(\"CLOSED\");\n },\n onError: () => {\n setMainConnectionState(\"CLOSED\");\n // Only show error message if we've previously connected successfully\n if (hasConnectedRefMain.current) {\n setErrorMessage(SERVER_CONNECTION_ERROR_MESSAGE, \"connection\");\n }\n },\n onMessage: handleMainMessage,\n };\n }, [\n handleMainMessage,\n setErrorMessage,\n clearConnectionError,\n sessionApiKey,\n initialAfterTimestamp,\n ]);\n\n // Separate WebSocket options for planning agent connection\n const planningWebsocketOptions: WebSocketHookOptions = useMemo(() => {\n const queryParams: Record<string, string | boolean> = {\n resend_all: true,\n };\n\n // Add session_api_key if available\n if (sessionApiKey) {\n queryParams.session_api_key = sessionApiKey;\n }\n\n const planningAgentConversation = subConversations?.[0];\n\n return {\n queryParams,\n reconnect: { enabled: true },\n onOpen: async () => {\n setPlanningConnectionState(\"OPEN\");\n hasConnectedRefPlanning.current = true; // Mark that we've successfully connected\n clearConnectionError(); // Clear a previous connection error; keep sticky conversation errors\n\n // Fetch expected event count for history loading detection\n if (\n planningAgentConversation?.id &&\n planningAgentConversation.conversation_url\n ) {\n try {\n const count = await EventService.getEventCount(\n planningAgentConversation.id,\n planningAgentConversation.conversation_url,\n planningAgentConversation.session_api_key,\n );\n setExpectedEventCountPlanning(count);\n\n // If no events expected, mark as loaded immediately\n if (count === 0) {\n setIsLoadingHistoryPlanning(false);\n }\n } catch (error) {\n // Fall back to marking as loaded to avoid infinite loading state\n setIsLoadingHistoryPlanning(false);\n }\n }\n },\n onClose: () => {\n setPlanningConnectionState(\"CLOSED\");\n },\n onError: () => {\n setPlanningConnectionState(\"CLOSED\");\n // Only show error message if we've previously connected successfully\n if (hasConnectedRefPlanning.current) {\n setErrorMessage(SERVER_CONNECTION_ERROR_MESSAGE, \"connection\");\n }\n },\n onMessage: handlePlanningMessage,\n };\n }, [\n handlePlanningMessage,\n setErrorMessage,\n clearConnectionError,\n sessionApiKey,\n subConversations,\n ]);\n\n // Only attempt WebSocket connection when we have a valid URL\n // This prevents connection attempts during task polling phase\n const websocketUrl = wsUrl;\n const { socket: mainSocket, reconnect: reconnectMain } = useWebSocket(\n websocketUrl || \"\",\n mainWebsocketOptions,\n );\n\n const { socket: planningAgentSocket, reconnect: reconnectPlanning } =\n useWebSocket(planningAgentWsUrl || \"\", planningWebsocketOptions);\n\n const reconnect = useCallback(() => {\n removeErrorMessage();\n const currentMode = useConversationStore.getState().conversationMode;\n if (currentMode === \"plan\" && planningAgentWsUrl) {\n reconnectPlanning();\n return;\n }\n reconnectMain();\n }, [\n planningAgentWsUrl,\n reconnectMain,\n reconnectPlanning,\n removeErrorMessage,\n ]);\n\n // V1 send message function via WebSocket\n // Falls back to REST API queue when WebSocket is not connected\n const sendMessage = useCallback(\n async (message: SendMessageRequest): Promise<SendMessageResult> => {\n const currentMode = useConversationStore.getState().conversationMode;\n const currentSocket =\n currentMode === \"plan\" ? planningAgentSocket : mainSocket;\n\n if (currentSocket?.readyState !== WebSocket.OPEN) {\n // WebSocket not connected - queue message via REST API\n // Message will be delivered automatically when conversation becomes ready\n if (!conversationId) {\n const error = new Error(\"No conversation ID available\");\n setErrorMessage(error.message);\n throw error;\n }\n\n try {\n await new ConversationClient(getAgentServerClientOptions()).sendEvent(\n conversationId,\n {\n role: \"user\",\n content: message.content,\n },\n { run: true },\n );\n // Message queued successfully - it will be delivered when ready\n // Return queued: true so caller knows not to show optimistic UI\n return { queued: true };\n } catch (error) {\n const errorMessage =\n error instanceof Error\n ? error.message\n : \"Failed to queue message for delivery\";\n setErrorMessage(errorMessage);\n throw error;\n }\n }\n\n try {\n // Send message through WebSocket as JSON with run: true so the\n // agent loop starts automatically in async mode.\n currentSocket.send(JSON.stringify({ ...message, run: true }));\n return { queued: false };\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : \"Failed to send message\";\n setErrorMessage(errorMessage);\n throw error;\n }\n },\n [mainSocket, planningAgentSocket, setErrorMessage, conversationId],\n );\n\n // Track main socket state changes\n useEffect(() => {\n // Only process socket updates if we have a valid URL and socket\n if (mainSocket && wsUrl) {\n // Update state based on socket readyState\n const updateState = () => {\n switch (mainSocket.readyState) {\n case WebSocket.CONNECTING:\n setMainConnectionState(\"CONNECTING\");\n break;\n case WebSocket.OPEN:\n setMainConnectionState(\"OPEN\");\n break;\n case WebSocket.CLOSING:\n setMainConnectionState(\"CLOSING\");\n break;\n case WebSocket.CLOSED:\n setMainConnectionState(\"CLOSED\");\n break;\n default:\n setMainConnectionState(\"CLOSED\");\n break;\n }\n };\n\n updateState();\n }\n }, [mainSocket, wsUrl]);\n\n // Track planning agent socket state changes\n useEffect(() => {\n // Only process socket updates if we have a valid URL and socket\n if (planningAgentSocket && planningAgentWsUrl) {\n // Update state based on socket readyState\n const updateState = () => {\n switch (planningAgentSocket.readyState) {\n case WebSocket.CONNECTING:\n setPlanningConnectionState(\"CONNECTING\");\n break;\n case WebSocket.OPEN:\n setPlanningConnectionState(\"OPEN\");\n break;\n case WebSocket.CLOSING:\n setPlanningConnectionState(\"CLOSING\");\n break;\n case WebSocket.CLOSED:\n setPlanningConnectionState(\"CLOSED\");\n break;\n default:\n setPlanningConnectionState(\"CLOSED\");\n break;\n }\n };\n\n updateState();\n }\n }, [planningAgentSocket, planningAgentWsUrl]);\n\n const contextValue = useMemo(\n () => ({ connectionState, sendMessage, isLoadingHistory, reconnect }),\n [connectionState, sendMessage, isLoadingHistory, reconnect],\n );\n\n return (\n <ConversationWebSocketContext.Provider value={contextValue}>\n {children}\n </ConversationWebSocketContext.Provider>\n );\n}\n\nexport const useConversationWebSocket =\n (): ConversationWebSocketContextType | null => {\n const context = useContext(ConversationWebSocketContext);\n // Return null instead of throwing when not in provider\n // This allows the hook to be called conditionally based on conversation version\n return context || null;\n };\n"],"mappings":"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"}