@openhands/agent-canvas 1.0.0-alpha.8 → 1.0.0-beta.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 (409) hide show
  1. package/README.md +17 -6
  2. package/bin/agent-canvas.mjs +22 -2
  3. package/build/assets/{QueryClientProvider-B7kl84Kj.js → QueryClientProvider-CkGuhXg-.js} +1 -1
  4. package/build/assets/{Trans-1j65oy9O.js → Trans-Cvm_-SMi.js} +1 -1
  5. package/build/assets/acp-providers-CbiRekh9.js +1 -0
  6. package/build/assets/{acp-route-guard-CQTmeJwM.js → acp-route-guard-B2yoBZ_4.js} +1 -1
  7. package/build/assets/{active-backend-context-TVbjnvmP.js → active-backend-context-cCM1vYYZ.js} +1 -1
  8. package/build/assets/add-backend-modal-DIUQzMPa.js +1 -0
  9. package/build/assets/agent-server-client-options-Bc5ZorQZ.js +1 -0
  10. package/build/assets/agent-server-compatibility-BlkUsrX2.js +1 -0
  11. package/build/assets/agent-server-conversation-service.api-DFvqqEDo.js +5 -0
  12. package/build/assets/{agent-settings-B247S9G3.js → agent-settings-CnGSCmK8.js} +1 -1
  13. package/build/assets/{alert-banner-BWoqueRw.js → alert-banner-DtzAX654.js} +1 -1
  14. package/build/assets/{analytics-consent-form-modal-C7sXfxRh.js → analytics-consent-form-modal-CHZ3I37v.js} +1 -1
  15. package/build/assets/api-key-entry-screen-B2gynaCp.js +1 -0
  16. package/build/assets/{app-settings-BVeSaty9.js → app-settings-Db9ITeJH.js} +1 -1
  17. package/build/assets/{automation-detail-g5-RZ0da.js → automation-detail-Di7EOIZD.js} +1 -1
  18. package/build/assets/{automations-list-DHoq_0MM.js → automations-list-IsIWdDiw.js} +1 -1
  19. package/build/assets/backend-form-modal-Dnk33xA_.js +1 -0
  20. package/build/assets/{backend-synced-settings-badge-nAfiUWvM.js → backend-synced-settings-badge-Dc6c7GT4.js} +1 -1
  21. package/build/assets/{base-modal-CQRvRHu1.js → base-modal-_dYTw1ri.js} +1 -1
  22. package/build/assets/{brand-button-C2nEKopC.js → brand-button-Br7f0kZJ.js} +1 -1
  23. package/build/assets/browser-D810xUYt.js +5 -0
  24. package/build/assets/browser-store-Couc4S5D.js +1 -0
  25. package/build/assets/browser-tab-B-aIqXRl.js +1 -0
  26. package/build/assets/{checkmark-BJJrZUF8.js → checkmark-DL7acQA7.js} +1 -1
  27. package/build/assets/{chevron-left-small-CSh-sE9L.js → chevron-left-small-CVWf8TI6.js} +1 -1
  28. package/build/assets/{circle-plus-check-toggle-qs8Va1cC.js → circle-plus-check-toggle-P7ZZToV4.js} +1 -1
  29. package/build/assets/{clock-ZR4Kn-_Y.js → clock-BRjCgHTc.js} +1 -1
  30. package/build/assets/{close-BdmyeRqS.js → close-B5LROHR3.js} +1 -1
  31. package/build/assets/{combobox-caret-B53O9Hsq.js → combobox-caret-to1O8irE.js} +1 -1
  32. package/build/assets/{condenser-settings-A35V3yng.js → condenser-settings-wnEKhBof.js} +1 -1
  33. package/build/assets/{confirmation-modal-C9-La0h3.js → confirmation-modal-Dau3w_sa.js} +1 -1
  34. package/build/assets/{context-menu-list-item-Buu9nc0q.js → context-menu-list-item-CWNFpuiC.js} +1 -1
  35. package/build/assets/conversation-HlncOV7n.js +19 -0
  36. package/build/assets/conversation-MtnkpqA9.js +1 -0
  37. package/build/assets/conversation-panel-DxnM6tRe.js +1 -0
  38. package/build/assets/{conversation-service.api-C8pYCyV6.js → conversation-service.api-nb5W1PqR.js} +1 -1
  39. package/build/assets/{conversation-tab-empty-state-D8dNvo-V.js → conversation-tab-empty-state-DyssnnWa.js} +1 -1
  40. package/build/assets/conversation-websocket-context-C8_PkGLi.js +3 -0
  41. package/build/assets/{copy-C7Ti2d8C.js → copy-DYgmUdIw.js} +1 -1
  42. package/build/assets/{custom-toast-handlers-BOc3qeQ7.js → custom-toast-handlers-C-SZFmto.js} +1 -1
  43. package/build/assets/declaration-BNMqORFE.js +1 -0
  44. package/build/assets/{device-verify-CMusn8nX.js → device-verify-DqDlphsG.js} +1 -1
  45. package/build/assets/{dist-DZHSA2e6.js → dist-C6t0EXL7.js} +1 -1
  46. package/build/assets/{edit-automation-modal-Dnjxbjn7.js → edit-automation-modal-BGzR3nfZ.js} +1 -1
  47. package/build/assets/{ellipsis-button-ugUATsNo.js → ellipsis-button-ZyLMPURn.js} +1 -1
  48. package/build/assets/{entry.client-D9uR9Blz.js → entry.client-1VMHpktY.js} +2 -2
  49. package/build/assets/{enum-filter-dropdown-1vpOGySB.js → enum-filter-dropdown-CEgCdu4A.js} +1 -1
  50. package/build/assets/{environment-switch-overlay-CTCTQikP.js → environment-switch-overlay-XL8yCGP6.js} +1 -1
  51. package/build/assets/{extensions-hub-BSUseHVF.js → extensions-hub-C651jsVh.js} +1 -1
  52. package/build/assets/{extensions-navigation-CT1kc1u_.js → extensions-navigation-BYR8Giqq.js} +1 -1
  53. package/build/assets/files-tab-BhnLgimi.js +1 -0
  54. package/build/assets/{folder-0WSMImNX.js → folder-ZZJVGgd7.js} +1 -1
  55. package/build/assets/git-control-bar-branch-button-M34A5_vX.js +27 -0
  56. package/build/assets/{git-provider-icon-DYE9n7fs.js → git-provider-icon-D5dCNy-k.js} +1 -1
  57. package/build/assets/home-CYQv7yc_.js +1 -0
  58. package/build/assets/{i18n-DjAGhTis.js → i18n-CTohRuoO.js} +1 -1
  59. package/build/assets/install-server-modal-f31_CLrW.js +1 -0
  60. package/build/assets/{launch-hZ0ifhcV.js → launch-DHEUYn2A.js} +1 -1
  61. package/build/assets/{lesson-plan-DRYG5SLI.js → lesson-plan-dH5Bj0pN.js} +1 -1
  62. package/build/assets/{link-external-Df8J52xI.js → link-external-D2POYx4c.js} +1 -1
  63. package/build/assets/{llm-client-ChQzg4wX.js → llm-client-DaH1TuyR.js} +1 -1
  64. package/build/assets/llm-settings-Bql-vydt.js +1 -0
  65. package/build/assets/llm-settings-C_tal6Ds.js +1 -0
  66. package/build/assets/{loading-spinner-C04FGh14.js → loading-spinner-BPtYORNK.js} +1 -1
  67. package/build/assets/{manage-backends-modal-rYeyGx7j.js → manage-backends-modal-l7RkKfwX.js} +1 -1
  68. package/build/assets/{manage-workspaces-modal-C5EuW8m1.js → manage-workspaces-modal-DhKF_8z3.js} +1 -1
  69. package/build/assets/manifest-9fee01b9.js +1 -0
  70. package/build/assets/{markdown-renderer-CEX4Becj.js → markdown-renderer-DMzf2i4x.js} +1 -1
  71. package/build/assets/mcp-D2onbwVk.js +9 -0
  72. package/build/assets/{messages-T2ewVkbp.js → messages-BMzyOW2V.js} +1 -1
  73. package/build/assets/{modal-backdrop-DTYGVmOR.js → modal-backdrop-BAbgYsqB.js} +1 -1
  74. package/build/assets/{modal-body-YElmM1dV.js → modal-body-BI6Ru2Qr.js} +1 -1
  75. package/build/assets/{modal-close-button-C_GpQt9F.js → modal-close-button-t1Gh3qmL.js} +1 -1
  76. package/build/assets/{model-selector-DeMmw-Xa.js → model-selector-SM9IUz-q.js} +1 -1
  77. package/build/assets/{mutation-Cz7N4XAo.js → mutation-D0OogFCz.js} +1 -1
  78. package/build/assets/{navigation-context-DeIPtGPp.js → navigation-context-D0YWpT8d.js} +1 -1
  79. package/build/assets/{navigation-link-C9JD4PYD.js → navigation-link-Cn7KP3c5.js} +1 -1
  80. package/build/assets/{openhands-logo-CI5Fhn1W.js → openhands-logo-CnrF6LKb.js} +1 -1
  81. package/build/assets/{option-service.api-DsI1UW7N.js → option-service.api-KvY_mZMY.js} +1 -1
  82. package/build/assets/{organization-service.api-COwMPFg5.js → organization-service.api-DzYTHTYC.js} +1 -1
  83. package/build/assets/{path-utils-CqJboYxo.js → path-utils-YohAYyMv.js} +1 -1
  84. package/build/assets/{plan-components-DEjMuDDG.js → plan-components-atxXCF0R.js} +1 -1
  85. package/build/assets/{planner-tab-BrntFmb1.js → planner-tab-CFc-hV07.js} +1 -1
  86. package/build/assets/{profiles-client-BGkKEV9j.js → profiles-client-D6IkTJof.js} +1 -1
  87. package/build/assets/{providers-DXvCAN_u.js → providers-Bx6EfrzZ.js} +1 -1
  88. package/build/assets/{proxy-CurRmrqf.js → proxy-CxydCnis.js} +1 -1
  89. package/build/assets/{query-client-config-Ba7qAAoO.js → query-client-config-B7u9asM0.js} +1 -1
  90. package/build/assets/{recommended-automations-launcher-BI9NhG8Y.js → recommended-automations-launcher-sgvfU62c.js} +3 -3
  91. package/build/assets/root-BXWU99D-.js +2 -0
  92. package/build/assets/{root-layout-BLjAEgle.js → root-layout-DVepR4To.js} +2 -2
  93. package/build/assets/sdk-section-page-DOIKvwSL.js +1 -0
  94. package/build/assets/{sdk-settings-schema-QBYH-ONX.js → sdk-settings-schema-DsUf9wu1.js} +1 -1
  95. package/build/assets/{search-Cq_cFrDt.js → search-27Owlc3A.js} +1 -1
  96. package/build/assets/{secrets-service-Bwd5DeUs.js → secrets-service-BsnKFc2x.js} +1 -1
  97. package/build/assets/secrets-settings-Bz_UohPJ.js +1 -0
  98. package/build/assets/{server-client-C3mC8Hl3.js → server-client-DyAQ3NZ_.js} +1 -1
  99. package/build/assets/{settings-D7E2U5tK.js → settings-BYkVX7vW.js} +1 -1
  100. package/build/assets/{settings-client-CwjfwoiB.js → settings-client-C73C7IgV.js} +1 -1
  101. package/build/assets/{settings-dropdown-input-VwAXNrOb.js → settings-dropdown-input-BJYvGdg-.js} +1 -1
  102. package/build/assets/{settings-gear-BJwWR1ej.js → settings-gear-C77PgE_O.js} +1 -1
  103. package/build/assets/{settings-index-J-3BNR0W.js → settings-index-Dz0BmdJD.js} +1 -1
  104. package/build/assets/{settings-input-DBywAnA7.js → settings-input-Bn7F5C75.js} +1 -1
  105. package/build/assets/{settings-list-classes-BOS092DR.js → settings-list-classes-Bf80tWtc.js} +1 -1
  106. package/build/assets/{settings-modal-B8vgWDTe.js → settings-modal-Brzgh5Yw.js} +1 -1
  107. package/build/assets/{settings-section-header-context-32x6WTyL.js → settings-section-header-context-BgZe5YkE.js} +1 -1
  108. package/build/assets/{settings-service.api-FvJGK45W.js → settings-service.api-CZ3uWx4v.js} +1 -1
  109. package/build/assets/{settings-switch-DTKmHC8F.js → settings-switch-BeIKrWms.js} +1 -1
  110. package/build/assets/{shared-conversation-a0QV8o99.js → shared-conversation-DChOdb0t.js} +1 -1
  111. package/build/assets/{sidebar-mobile-menu-toggle-DTUNI1WQ.js → sidebar-mobile-menu-toggle-BWuf4PRH.js} +1 -1
  112. package/build/assets/{sidebar-nav-link-CnWoZcwc.js → sidebar-nav-link-BGjiJq-4.js} +1 -1
  113. package/build/assets/{skill-card-pill-row-tZ599jli.js → skill-card-pill-row-DF1axQCG.js} +1 -1
  114. package/build/assets/{skills-ZyAO5dyK.js → skills-ChIKZPK4.js} +1 -1
  115. package/build/assets/{skills-plugins-BSRz041I.js → skills-plugins-CcI_19lM.js} +1 -1
  116. package/build/assets/{skills-settings-DOnMn9q1.js → skills-settings-DlA5hlXw.js} +1 -1
  117. package/build/assets/{status-CsatcFbK.js → status-hp6M6E7E.js} +1 -1
  118. package/build/assets/{styled-tooltip-CS3mB_1X.js → styled-tooltip-CBzrri6o.js} +1 -1
  119. package/build/assets/{switch-skeleton-C-CfhYYV.js → switch-skeleton-DnC9wLp7.js} +1 -1
  120. package/build/assets/{task-list-tab-Day9nhRT.js → task-list-tab-DUJn1sgz.js} +1 -1
  121. package/build/assets/{terminal-ro4SNjUU.js → terminal-CRf9S0Z2.js} +1 -1
  122. package/build/assets/{terminal-LNa-iU5c.js → terminal-RmuaSdhJ.js} +1 -1
  123. package/build/assets/{toggle-switch-k-IZCDbt.js → toggle-switch-Pvyp2RAN.js} +1 -1
  124. package/build/assets/{typography-vVUMoNUg.js → typography-gpuWmrQO.js} +1 -1
  125. package/build/assets/{u-check-circle-DplbarS5.js → u-check-circle-IUIfACQQ.js} +1 -1
  126. package/build/assets/{u-check-circle-half-yDuiSZHC.js → u-check-circle-half-C1YxB6py.js} +1 -1
  127. package/build/assets/{u-circuit-C9tYkpeK.js → u-circuit-BmVikJHu.js} +1 -1
  128. package/build/assets/{u-edit-KAUlufD8.js → u-edit-CFvXHqZk.js} +1 -1
  129. package/build/assets/use-active-conversation-Db3IWSPK.js +1 -0
  130. package/build/assets/{use-agent-settings-schema-Bvp5UzV8.js → use-agent-settings-schema-33Un7UF2.js} +1 -1
  131. package/build/assets/{use-agent-state-DE5dlEXJ.js → use-agent-state-Bn8vS5sY.js} +1 -1
  132. package/build/assets/{use-cloud-current-user-id-DWVar4st.js → use-cloud-current-user-id-CvkXFnTT.js} +1 -1
  133. package/build/assets/use-config-Co1O8-Ey.js +1 -0
  134. package/build/assets/{use-create-conversation-DW7AGgLA.js → use-create-conversation-CKS3EAHu.js} +1 -1
  135. package/build/assets/{use-event-store-CQZCcVz-.js → use-event-store-BT_gV3ut.js} +1 -1
  136. package/build/assets/use-get-secrets-DuhdIA59.js +1 -0
  137. package/build/assets/{use-handle-plan-click-DpgEQDAV.js → use-handle-plan-click-C9zJpK8A.js} +1 -1
  138. package/build/assets/use-is-authed-BggE5wPj.js +1 -0
  139. package/build/assets/{use-is-creating-conversation-DhDeeWfA.js → use-is-creating-conversation-BZ5hB_Bg.js} +1 -1
  140. package/build/assets/{use-launch-skill-in-chat-DVGPFrbI.js → use-launch-skill-in-chat-fNN_xGZG.js} +1 -1
  141. package/build/assets/{use-llm-profiles-D3-KXwQ0.js → use-llm-profiles-DDOol3gK.js} +1 -1
  142. package/build/assets/use-runtime-is-ready-CQCE3xZC.js +1 -0
  143. package/build/assets/{use-save-settings-CEEKSTWG.js → use-save-settings-VUrj_QNG.js} +1 -1
  144. package/build/assets/{use-settings-DQ7Oo1Hj.js → use-settings-DQIZmIov.js} +1 -1
  145. package/build/assets/{use-settings-nav-items-YmrXrjn9.js → use-settings-nav-items-1ZvovKSr.js} +1 -1
  146. package/build/assets/use-skills-DAMLFjKU.js +1 -0
  147. package/build/assets/{use-task-list-Bs90uF2N.js → use-task-list-CLJbuJgM.js} +1 -1
  148. package/build/assets/use-unified-vscode-url-sZt29HrC.js +1 -0
  149. package/build/assets/use-user-conversation-DfgEB6RW.js +1 -0
  150. package/build/assets/{useMutation-B4OUESdw.js → useMutation-DqrumCWD.js} +1 -1
  151. package/build/assets/{useTranslation-CpIcQBq6.js → useTranslation-DCOdSSMl.js} +1 -1
  152. package/build/assets/{utils-D-HX7JCe.js → utils-i18rdUj2.js} +1 -1
  153. package/build/assets/v4-CNn21NXa.js +1 -0
  154. package/build/assets/{vendor~browser-Dr71AdrG.js → vendor~browser-BNjNhjFU.js} +1 -1
  155. package/build/assets/{vendor~browser-tab-BiVxfjJo.js → vendor~browser-tab-BgwV1mxF.js} +1 -1
  156. package/build/assets/{vendor~conversation-panel~conversation-BlCIz9XQ.js → vendor~conversation-panel~conversation-a9SyrrhV.js} +1 -1
  157. package/build/assets/{vendor~files-tab-DtLR-QD9.js → vendor~files-tab-BGKayPiK.js} +1 -1
  158. package/build/assets/{vendor~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-Ds9quNZ9.js → vendor~home~conversation-panel~conversation~shared-conversation~planner-tab~files-tab-smY2r837.js} +1 -1
  159. package/build/assets/{vendor~home~mcp~automations-list-C5PoHCy6.js → vendor~home~mcp~automations-list-Ccy2I0KU.js} +1 -1
  160. package/build/assets/{vendor~home~mcp~automations-list-BUBGGGYz.js → vendor~home~mcp~automations-list-DoPfwaXj.js} +1 -1
  161. package/build/assets/{vendor~home~mcp~llm-settings~agent-settings~condenser-settings~verification-settings~app-se~ocm3mykx-CGlZoBKa.js → vendor~home~mcp~llm-settings~agent-settings~condenser-settings~verification-settings~app-se~ocm3mykx-DbfELDJu.js} +2 -2
  162. package/build/assets/{vendor~home~mcp~llm-settings~agent-settings~condenser-settings~verification-settings~app-se~ocm3mykx-DE11mPxp.js → vendor~home~mcp~llm-settings~agent-settings~condenser-settings~verification-settings~app-se~ocm3mykx-Z3nsiNNq.js} +1 -1
  163. package/build/assets/{vendor~launch-Dg--Ssk6.js → vendor~launch-vdeRTWFu.js} +1 -1
  164. package/build/assets/{vendor~root-layout~conversation-panel~conversation~shared-conversation-DrXgiSCq.js → vendor~root-layout~conversation-panel~conversation~shared-conversation-DW31UyBp.js} +1 -1
  165. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~iguv7bgw-8b8V5bfO.js → vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~iguv7bgw-BkQGKpye.js} +1 -1
  166. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~iguv7bgw-Dy7L6fMG.js → vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~iguv7bgw-DzIXV3Ui.js} +1 -1
  167. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~k776hupu-D40EXhZx.js → vendor~root-layout~home~conversation-panel~conversation~extensions-hub~skills-settings~skil~k776hupu-Bbs7UJ5U.js} +2 -2
  168. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-settin~dp08i1qy-CHrEOFl6.js → vendor~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-settin~dp08i1qy-DTwbEEcX.js} +1 -1
  169. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~shared-conversation~alert-banner~pl~rqjteh0a-BP1SKG0F.js → vendor~root-layout~home~conversation-panel~conversation~shared-conversation~alert-banner~pl~rqjteh0a-d2oallMa.js} +1 -1
  170. package/build/assets/{vendor~root~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-s~kyz9p27j-CyUbhpbm.js → vendor~root~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-s~f2l2lr17-CDXvdvb2.js} +1 -1
  171. package/build/assets/{verification-settings-BtlTiHP8.js → verification-settings-CsbvQcYS.js} +1 -1
  172. package/build/assets/{vscode-tab-C0ShhiSU.js → vscode-tab-Zb-QbTuV.js} +1 -1
  173. package/build/assets/{waiting-for-runtime-message-DWPl_Yby.js → waiting-for-runtime-message-CntjExbU.js} +1 -1
  174. package/build/assets/{x-mark-CWI0f9yI.js → x-mark-CrpjscNc.js} +1 -1
  175. package/build/index.html +4 -4
  176. package/build/locales/ar/openhands.json +7 -0
  177. package/build/locales/ca/openhands.json +7 -0
  178. package/build/locales/de/openhands.json +7 -0
  179. package/build/locales/en/openhands.json +7 -0
  180. package/build/locales/es/openhands.json +7 -0
  181. package/build/locales/fr/openhands.json +7 -0
  182. package/build/locales/it/openhands.json +7 -0
  183. package/build/locales/ja/openhands.json +7 -0
  184. package/build/locales/ko-KR/openhands.json +7 -0
  185. package/build/locales/no/openhands.json +7 -0
  186. package/build/locales/pt/openhands.json +7 -0
  187. package/build/locales/tr/openhands.json +7 -0
  188. package/build/locales/uk/openhands.json +7 -0
  189. package/build/locales/zh-CN/openhands.json +7 -0
  190. package/build/locales/zh-TW/openhands.json +7 -0
  191. package/config/defaults.json +0 -4
  192. package/dist/api/agent-server-adapter.cjs +1 -1
  193. package/dist/api/agent-server-adapter.cjs.map +1 -1
  194. package/dist/api/agent-server-adapter.js +2 -1
  195. package/dist/api/agent-server-adapter.js.map +1 -1
  196. package/dist/api/agent-server-compatibility.cjs +1 -1
  197. package/dist/api/agent-server-compatibility.cjs.map +1 -1
  198. package/dist/api/agent-server-compatibility.d.ts +16 -0
  199. package/dist/api/agent-server-compatibility.js +31 -20
  200. package/dist/api/agent-server-compatibility.js.map +1 -1
  201. package/dist/api/agent-server-config.cjs +1 -1
  202. package/dist/api/agent-server-config.cjs.map +1 -1
  203. package/dist/api/agent-server-config.d.ts +45 -0
  204. package/dist/api/agent-server-config.js +49 -21
  205. package/dist/api/agent-server-config.js.map +1 -1
  206. package/dist/api/backend-registry/storage.cjs +1 -1
  207. package/dist/api/backend-registry/storage.cjs.map +1 -1
  208. package/dist/api/backend-registry/storage.js +34 -32
  209. package/dist/api/backend-registry/storage.js.map +1 -1
  210. package/dist/api/conversation-service/agent-server-conversation-service.api.cjs +1 -1
  211. package/dist/api/conversation-service/agent-server-conversation-service.api.cjs.map +1 -1
  212. package/dist/api/conversation-service/agent-server-conversation-service.api.d.ts +5 -4
  213. package/dist/api/conversation-service/agent-server-conversation-service.api.js +70 -76
  214. package/dist/api/conversation-service/agent-server-conversation-service.api.js.map +1 -1
  215. package/dist/api/skills-service.cjs +1 -1
  216. package/dist/api/skills-service.cjs.map +1 -1
  217. package/dist/api/skills-service.d.ts +1 -1
  218. package/dist/api/skills-service.js +2 -2
  219. package/dist/api/skills-service.js.map +1 -1
  220. package/dist/components/features/backends/api-key-entry-screen.d.ts +10 -0
  221. package/dist/components/features/backends/backend-form-modal.cjs +1 -1
  222. package/dist/components/features/backends/backend-form-modal.cjs.map +1 -1
  223. package/dist/components/features/backends/backend-form-modal.d.ts +23 -2
  224. package/dist/components/features/backends/backend-form-modal.js +185 -173
  225. package/dist/components/features/backends/backend-form-modal.js.map +1 -1
  226. package/dist/components/features/browser/browser.cjs +1 -1
  227. package/dist/components/features/browser/browser.cjs.map +1 -1
  228. package/dist/components/features/browser/browser.js +10 -16
  229. package/dist/components/features/browser/browser.js.map +1 -1
  230. package/dist/components/features/conversation-panel/skills-modal.cjs +1 -1
  231. package/dist/components/features/conversation-panel/skills-modal.cjs.map +1 -1
  232. package/dist/components/features/conversation-panel/skills-modal.js +1 -1
  233. package/dist/components/features/conversation-panel/skills-modal.js.map +1 -1
  234. package/dist/components/features/mcp-page/install-server-modal.cjs +1 -1
  235. package/dist/components/features/mcp-page/install-server-modal.cjs.map +1 -1
  236. package/dist/components/features/mcp-page/install-server-modal.js +123 -116
  237. package/dist/components/features/mcp-page/install-server-modal.js.map +1 -1
  238. package/dist/components/features/mcp-page/installed-server-card.cjs +1 -1
  239. package/dist/components/features/mcp-page/installed-server-card.cjs.map +1 -1
  240. package/dist/components/features/mcp-page/installed-server-card.js +40 -40
  241. package/dist/components/features/mcp-page/installed-server-card.js.map +1 -1
  242. package/dist/components/features/mcp-page/marketplace-card.cjs +1 -1
  243. package/dist/components/features/mcp-page/marketplace-card.cjs.map +1 -1
  244. package/dist/components/features/mcp-page/marketplace-card.js +2 -3
  245. package/dist/components/features/mcp-page/marketplace-card.js.map +1 -1
  246. package/dist/components/features/mcp-page/marketplace-section.cjs +1 -1
  247. package/dist/components/features/mcp-page/marketplace-section.cjs.map +1 -1
  248. package/dist/components/features/mcp-page/marketplace-section.js +21 -21
  249. package/dist/components/features/mcp-page/marketplace-section.js.map +1 -1
  250. package/dist/components/features/onboarding/steps/setup-acp-secrets-step.d.ts +27 -0
  251. package/dist/components/features/settings/llm-profiles/llm-settings-local-view.cjs +1 -1
  252. package/dist/components/features/settings/llm-profiles/llm-settings-local-view.js +2 -0
  253. package/dist/components/features/settings/sdk-settings/sdk-section-page.cjs +1 -1
  254. package/dist/components/features/settings/sdk-settings/sdk-section-page.cjs.map +1 -1
  255. package/dist/components/features/settings/sdk-settings/sdk-section-page.d.ts +10 -1
  256. package/dist/components/features/settings/sdk-settings/sdk-section-page.js +87 -84
  257. package/dist/components/features/settings/sdk-settings/sdk-section-page.js.map +1 -1
  258. package/dist/constants/acp-providers.cjs +1 -1
  259. package/dist/constants/acp-providers.cjs.map +1 -1
  260. package/dist/constants/acp-providers.d.ts +25 -0
  261. package/dist/constants/acp-providers.js +1 -0
  262. package/dist/constants/acp-providers.js.map +1 -1
  263. package/dist/contexts/conversation-websocket-context.cjs +3 -3
  264. package/dist/contexts/conversation-websocket-context.cjs.map +1 -1
  265. package/dist/contexts/conversation-websocket-context.js +177 -165
  266. package/dist/contexts/conversation-websocket-context.js.map +1 -1
  267. package/dist/hooks/chat/use-model-interceptor.cjs.map +1 -1
  268. package/dist/hooks/chat/use-model-interceptor.js.map +1 -1
  269. package/dist/hooks/chat/use-slash-command.cjs +1 -1
  270. package/dist/hooks/chat/use-slash-command.cjs.map +1 -1
  271. package/dist/hooks/chat/use-slash-command.js +1 -1
  272. package/dist/hooks/chat/use-slash-command.js.map +1 -1
  273. package/dist/hooks/mutation/use-switch-llm-profile.cjs.map +1 -1
  274. package/dist/hooks/mutation/use-switch-llm-profile.d.ts +1 -1
  275. package/dist/hooks/mutation/use-switch-llm-profile.js.map +1 -1
  276. package/dist/hooks/query/use-config.cjs +1 -1
  277. package/dist/hooks/query/use-config.cjs.map +1 -1
  278. package/dist/hooks/query/use-config.js +10 -10
  279. package/dist/hooks/query/use-config.js.map +1 -1
  280. package/dist/hooks/query/use-conversation-skills.cjs +2 -0
  281. package/dist/hooks/query/use-conversation-skills.cjs.map +1 -0
  282. package/dist/hooks/query/use-conversation-skills.d.ts +7 -0
  283. package/dist/hooks/query/use-conversation-skills.js +8 -0
  284. package/dist/hooks/query/use-conversation-skills.js.map +1 -0
  285. package/dist/hooks/query/use-local-git-info.cjs +3 -3
  286. package/dist/hooks/query/use-local-git-info.cjs.map +1 -1
  287. package/dist/hooks/query/use-local-git-info.js +24 -25
  288. package/dist/hooks/query/use-local-git-info.js.map +1 -1
  289. package/dist/hooks/query/use-skills.cjs +1 -1
  290. package/dist/hooks/query/use-skills.cjs.map +1 -1
  291. package/dist/hooks/query/use-skills.d.ts +6 -1
  292. package/dist/hooks/query/use-skills.js +3 -3
  293. package/dist/hooks/query/use-skills.js.map +1 -1
  294. package/dist/i18n/declaration.cjs +1 -1
  295. package/dist/i18n/declaration.cjs.map +1 -1
  296. package/dist/i18n/declaration.d.ts +7 -0
  297. package/dist/i18n/declaration.js +1 -1
  298. package/dist/i18n/declaration.js.map +1 -1
  299. package/dist/i18n/translation.cjs +2 -2
  300. package/dist/i18n/translation.cjs.map +1 -1
  301. package/dist/i18n/translation.js +119 -0
  302. package/dist/i18n/translation.js.map +1 -1
  303. package/dist/locales/ar/openhands.json +7 -0
  304. package/dist/locales/ca/openhands.json +7 -0
  305. package/dist/locales/de/openhands.json +7 -0
  306. package/dist/locales/en/openhands.json +7 -0
  307. package/dist/locales/es/openhands.json +7 -0
  308. package/dist/locales/fr/openhands.json +7 -0
  309. package/dist/locales/it/openhands.json +7 -0
  310. package/dist/locales/ja/openhands.json +7 -0
  311. package/dist/locales/ko-KR/openhands.json +7 -0
  312. package/dist/locales/no/openhands.json +7 -0
  313. package/dist/locales/pt/openhands.json +7 -0
  314. package/dist/locales/tr/openhands.json +7 -0
  315. package/dist/locales/uk/openhands.json +7 -0
  316. package/dist/locales/zh-CN/openhands.json +7 -0
  317. package/dist/locales/zh-TW/openhands.json +7 -0
  318. package/dist/package.cjs +1 -1
  319. package/dist/package.cjs.map +1 -1
  320. package/dist/package.js +3 -3
  321. package/dist/package.js.map +1 -1
  322. package/dist/routes/conversation.cjs +1 -1
  323. package/dist/routes/conversation.cjs.map +1 -1
  324. package/dist/routes/conversation.js +61 -63
  325. package/dist/routes/conversation.js.map +1 -1
  326. package/dist/routes/mcp.cjs +1 -1
  327. package/dist/routes/mcp.cjs.map +1 -1
  328. package/dist/routes/mcp.js +64 -64
  329. package/dist/routes/mcp.js.map +1 -1
  330. package/dist/stores/browser-store.cjs +1 -1
  331. package/dist/stores/browser-store.cjs.map +1 -1
  332. package/dist/stores/browser-store.js +1 -1
  333. package/dist/stores/browser-store.js.map +1 -1
  334. package/dist/stores/use-event-store.cjs +1 -1
  335. package/dist/stores/use-event-store.cjs.map +1 -1
  336. package/dist/stores/use-event-store.d.ts +22 -0
  337. package/dist/stores/use-event-store.js +9 -1
  338. package/dist/stores/use-event-store.js.map +1 -1
  339. package/dist/ui/context-menu.d.ts +1 -1
  340. package/dist/ui/help-link.d.ts +1 -1
  341. package/dist/utils/mcp-marketplace-utils.cjs +1 -1
  342. package/dist/utils/mcp-marketplace-utils.cjs.map +1 -1
  343. package/dist/utils/mcp-marketplace-utils.d.ts +13 -22
  344. package/dist/utils/mcp-marketplace-utils.js +46 -28
  345. package/dist/utils/mcp-marketplace-utils.js.map +1 -1
  346. package/dist/utils/sdk-settings-schema.cjs +1 -1
  347. package/dist/utils/sdk-settings-schema.cjs.map +1 -1
  348. package/dist/utils/sdk-settings-schema.d.ts +1 -0
  349. package/dist/utils/sdk-settings-schema.js +1 -1
  350. package/dist/utils/sdk-settings-schema.js.map +1 -1
  351. package/package.json +3 -3
  352. package/scripts/dev-safe.mjs +94 -57
  353. package/scripts/dev-static.mjs +2 -3
  354. package/scripts/dev-with-automation.mjs +98 -67
  355. package/scripts/static-server.mjs +77 -35
  356. package/tools/canvas_ui_tool.py +4 -0
  357. package/build/assets/acp-providers-DauuOsW9.js +0 -1
  358. package/build/assets/add-backend-modal-KMmPQNZU.js +0 -1
  359. package/build/assets/agent-server-client-options-DT2GP6VJ.js +0 -1
  360. package/build/assets/agent-server-compatibility-2aOx5iWd.js +0 -1
  361. package/build/assets/agent-server-conversation-service.api-DSl9G5UR.js +0 -5
  362. package/build/assets/backend-form-modal-K6IMCr3p.js +0 -1
  363. package/build/assets/browser-DKG63inJ.js +0 -5
  364. package/build/assets/browser-store-C3AqxAO7.js +0 -1
  365. package/build/assets/browser-tab-B_BuTvrO.js +0 -1
  366. package/build/assets/conversation-BD5WemJI.js +0 -19
  367. package/build/assets/conversation-C47K62n8.js +0 -1
  368. package/build/assets/conversation-panel-Dn-S56Gk.js +0 -1
  369. package/build/assets/conversation-websocket-context-Ywrxd_9p.js +0 -3
  370. package/build/assets/declaration-D378OjpZ.js +0 -1
  371. package/build/assets/files-tab-B3A1NDlZ.js +0 -1
  372. package/build/assets/git-control-bar-branch-button-CcIpmyfM.js +0 -27
  373. package/build/assets/home-dIzxi5Dd.js +0 -1
  374. package/build/assets/install-server-modal-z5VaHeXd.js +0 -1
  375. package/build/assets/llm-settings-2036m7Wt.js +0 -1
  376. package/build/assets/llm-settings-CcHqGOYL.js +0 -1
  377. package/build/assets/manifest-97e839da.js +0 -1
  378. package/build/assets/mcp-C06YssEI.js +0 -9
  379. package/build/assets/root-BS1Td78t.js +0 -2
  380. package/build/assets/sdk-section-page-CJW0G04-.js +0 -1
  381. package/build/assets/secrets-settings-MLXqOtX2.js +0 -1
  382. package/build/assets/use-active-conversation-D15D9GgR.js +0 -1
  383. package/build/assets/use-config-BSu_53GL.js +0 -1
  384. package/build/assets/use-conversation-id-DajhCn2A.js +0 -1
  385. package/build/assets/use-is-authed-hXC8vxgT.js +0 -1
  386. package/build/assets/use-runtime-is-ready-XFbT16BD.js +0 -1
  387. package/build/assets/use-skills-Xe0vjPMt.js +0 -1
  388. package/build/assets/use-unified-vscode-url-BOsIOd-b.js +0 -1
  389. package/build/assets/use-user-conversation-Mc0mQgkl.js +0 -1
  390. /package/build/assets/{automation-XLxhq3I8.js → automation-IdgZq6ZK.js} +0 -0
  391. /package/build/assets/{common-SMkEaBSr.js → common-DR1t-EeP.js} +0 -0
  392. /package/build/assets/{conversation-state-store-Bc0slAjL.js → conversation-state-store-u5jepov0.js} +0 -0
  393. /package/build/assets/{dist-yMQV8IUk.js → dist-BxBP7tFD.js} +0 -0
  394. /package/build/assets/{git-status-mapper-BI8FyUVp.js → git-status-mapper-DnL9OC8_.js} +0 -0
  395. /package/build/assets/{handle-capture-consent-BfZATzpI.js → handle-capture-consent-3XrjZ8wi.js} +0 -0
  396. /package/build/assets/{iconBase-C7N9pPOs.js → iconBase-DE30Zj_-.js} +0 -0
  397. /package/build/assets/{settings-D5am1n6X.js → settings-D_H-qsRm.js} +0 -0
  398. /package/build/assets/{settings-like-page-layout-classes-Bn-M9oOa.js → settings-like-page-layout-classes-I0BDBEoq.js} +0 -0
  399. /package/build/assets/{settings-utils-BsvSU3OM.js → settings-utils-B6Nl07io.js} +0 -0
  400. /package/build/assets/{sidebar-store-cOeaKmIm.js → sidebar-store-Uy3v0AOV.js} +0 -0
  401. /package/build/assets/{use-breakpoint-B86yKT9n.js → use-breakpoint-DbJ6FkQ-.js} +0 -0
  402. /package/build/assets/{use-click-outside-element-835W9pC6.js → use-click-outside-element-DffgWWoZ.js} +0 -0
  403. /package/build/assets/{vendor~browser-BpdPBhgZ.js → vendor~browser-DDiZgqD3.js} +0 -0
  404. /package/build/assets/{vendor~conversation-panel~conversation~alert-banner-Df7_G0zR.js → vendor~conversation-panel~conversation~alert-banner-DbvX3OcM.js} +0 -0
  405. /package/build/assets/{vendor~entry.client~root~root-layout~home~conversation-panel~conversation~launch~extensions~b4cctr4k-B7YVdv1X.js → vendor~entry.client~root~root-layout~home~conversation-panel~conversation~launch~extensions~g56ukk6u-DsSvIDZQ.js} +0 -0
  406. /package/build/assets/{vendor~entry.client~root~root-layout~home~conversation-panel~conversation~launch~extensions~i9dbt75i-CI82Did1.js → vendor~entry.client~root~root-layout~home~conversation-panel~conversation~launch~extensions~hkqzh1hb-BZ0HXuHD.js} +0 -0
  407. /package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-settin~pfbaerbd-zhv9fooy.js → vendor~root-layout~home~conversation-panel~conversation~launch~extensions-hub~skills-settin~ninslayh-D9P8e98a.js} +0 -0
  408. /package/build/assets/{vendor~terminal-BUxzHKcC.js → vendor~terminal-DUrOWGFE.js} +0 -0
  409. /package/build/assets/{vscode-url-helper-jesbpos5.js → vscode-url-helper-Cwy1A62q.js} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"sdk-settings-schema.cjs","names":[],"sources":["../../src/utils/sdk-settings-schema.ts"],"sourcesContent":["import {\n SettingProminence,\n Settings,\n SettingsFieldSchema,\n SettingsSchema,\n SettingsSectionSchema,\n SettingsValue,\n} from \"#/types/settings\";\nimport { getSettingsFieldConstraints } from \"#/utils/sdk-settings-field-metadata\";\n\nexport type SettingsFormValues = Record<string, string | boolean>;\nexport type SettingsDirtyState = Record<string, boolean>;\nexport type SdkSettingsPayload = Record<string, SettingsValue>;\nexport type SettingsValueSource = \"agent_settings\" | \"conversation_settings\";\n\nexport type SettingsView = \"basic\" | \"advanced\" | \"all\";\n\n/** Fields that are rendered by purpose-built components instead of the\n * generic `SchemaField` renderer. */\nexport const SPECIALLY_RENDERED_KEYS = new Set([\n \"llm.model\",\n \"llm.api_key\",\n \"llm.base_url\",\n]);\n\n/** Prominence tiers visible at each view level. */\nconst VIEW_PROMINENCES: Record<SettingsView, Set<SettingProminence>> = {\n basic: new Set<SettingProminence>([\"critical\"]),\n advanced: new Set<SettingProminence>([\"critical\", \"major\"]),\n all: new Set<SettingProminence>([\"critical\", \"major\", \"minor\"]),\n};\n\n/**\n * True when `schema` looks like a usable `SettingsSchema` — i.e. an\n * object with an array `sections` field. Guards every helper in this\n * module against malformed/empty schema responses (e.g. when the\n * frontend ends up pointing at a host that does not actually serve\n * `/api/settings/agent-schema`, such as an unconfigured Vercel preview\n * origin that returns the React Router SPA shell for arbitrary\n * `/api/*` paths). Without this check, `schema.sections.filter(...)`\n * inside `SdkSectionPage` blows up with\n * `Cannot read properties of undefined (reading 'filter')` and React\n * Router escalates to a full-screen error page.\n */\nexport function isValidSettingsSchema(\n schema: SettingsSchema | null | undefined,\n): schema is SettingsSchema {\n return !!schema && Array.isArray((schema as SettingsSchema).sections);\n}\n\nfunction getSchemaFields(schema: SettingsSchema): SettingsFieldSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections.flatMap((section) => section.fields);\n}\n\n/** Traverse a nested object using a dotted key path (e.g. \"llm.model\"). */\nfunction lookupDotted(\n obj: Record<string, unknown> | null | undefined,\n key: string,\n): unknown {\n if (!obj) return undefined;\n const parts = key.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/** Set a value in a nested object at a dotted key path (e.g. \"llm.model\"). */\nfunction setDotted(\n obj: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n const parts = key.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n\nexport function getSettingValue(\n settings: Settings,\n key: string,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsValue {\n return (lookupDotted(settings[source] as Record<string, unknown>, key) ??\n null) as SettingsValue;\n}\n\nexport function getAgentSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"agent_settings\");\n}\n\nexport function getConversationSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"conversation_settings\");\n}\n\nfunction isChoiceField(field: SettingsFieldSchema): boolean {\n return field.choices.length > 0;\n}\n\nfunction isCriticalField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"critical\";\n}\n\nfunction isMinorField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"minor\";\n}\n\nfunction normalizeFieldValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): string | boolean {\n const resolvedValue = rawValue ?? field.default;\n\n if (isChoiceField(field)) {\n return resolvedValue === null || resolvedValue === undefined\n ? \"\"\n : String(resolvedValue);\n }\n\n if (field.value_type === \"boolean\") {\n return Boolean(resolvedValue ?? false);\n }\n\n if (resolvedValue === null || resolvedValue === undefined) {\n return \"\";\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n return JSON.stringify(resolvedValue, null, 2);\n }\n\n return String(resolvedValue);\n}\n\nfunction normalizeComparableValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): boolean | number | string | null {\n if (rawValue === undefined) {\n return null;\n }\n\n if (field.value_type === \"boolean\") {\n if (typeof rawValue === \"string\") {\n if (rawValue === \"true\") {\n return true;\n }\n if (rawValue === \"false\") {\n return false;\n }\n }\n if (rawValue === null) {\n return null;\n }\n return Boolean(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n if (rawValue === \"\" || rawValue === null) {\n return null;\n }\n\n const parsedValue =\n typeof rawValue === \"number\" ? rawValue : Number(String(rawValue));\n return Number.isNaN(parsedValue) ? null : parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n if (rawValue === null) {\n return null;\n }\n\n // Treat empty objects as null so that serializer artifacts\n // (e.g. mcp_config: {} vs schema default null) don't trigger\n // a spurious view escalation in inferInitialView.\n if (\n field.value_type === \"object\" &&\n typeof rawValue === \"object\" &&\n !Array.isArray(rawValue) &&\n Object.keys(rawValue as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n\n if (typeof rawValue === \"string\") {\n const trimmedValue = rawValue.trim();\n if (!trimmedValue) {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(trimmedValue);\n // Also normalise stringified empty objects\n if (\n field.value_type === \"object\" &&\n parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed) &&\n Object.keys(parsed as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n return JSON.stringify(parsed);\n } catch {\n return trimmedValue;\n }\n }\n\n return JSON.stringify(rawValue);\n }\n\n if (rawValue === null) {\n return null;\n }\n\n return String(rawValue);\n}\n\nexport function buildInitialSettingsFormValues(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsFormValues {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return {};\n }\n\n return Object.fromEntries(\n getSchemaFields(schema).map((field) => [\n field.key,\n normalizeFieldValue(field, getSettingValue(settings, field.key, source)),\n ]),\n );\n}\n\nexport function inferInitialView(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsView {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return \"basic\";\n }\n\n let hasMinorOverride = false;\n let hasMajorOverride = false;\n\n for (const field of getSchemaFields(schema)) {\n if (!isCriticalField(field)) {\n const currentValue = getSettingValue(settings, field.key, source);\n const isDifferent =\n normalizeComparableValue(\n field,\n currentValue ?? field.default ?? null,\n ) !== normalizeComparableValue(field, field.default ?? null);\n\n if (isDifferent) {\n if (isMinorField(field)) {\n hasMinorOverride = true;\n } else {\n hasMajorOverride = true;\n }\n }\n }\n }\n\n if (hasMinorOverride) return \"all\";\n if (hasMajorOverride) return \"advanced\";\n return \"basic\";\n}\n\n/** Determine which view tier to default to based on whether the user has\n * overridden any non-critical settings. */\nexport function hasAdvancedSettingsOverrides(settings: Settings): boolean {\n return inferInitialView(settings) !== \"basic\";\n}\n\nexport function isSettingsFieldVisible(\n field: SettingsFieldSchema,\n values: SettingsFormValues,\n): boolean {\n return field.depends_on.every((dependency) => values[dependency] === true);\n}\n\nfunction parseBooleanFieldValue(rawValue: string | boolean): boolean | null {\n if (typeof rawValue === \"boolean\") {\n return rawValue;\n }\n\n const normalizedValue = rawValue.trim().toLowerCase();\n if (!normalizedValue) {\n return null;\n }\n if (normalizedValue === \"true\") {\n return true;\n }\n if (normalizedValue === \"false\") {\n return false;\n }\n\n throw new Error(`Expected a boolean value, received: ${rawValue}`);\n}\n\nfunction coerceFieldValue(\n field: SettingsFieldSchema,\n rawValue: string | boolean,\n): SettingsValue {\n if (field.value_type === \"boolean\") {\n return parseBooleanFieldValue(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n const parsedValue = Number(stringValue);\n if (Number.isNaN(parsedValue)) {\n throw new Error(`Expected a numeric value, received: ${stringValue}`);\n }\n if (field.value_type === \"integer\" && !Number.isInteger(parsedValue)) {\n throw new Error(`Expected an integer value, received: ${stringValue}`);\n }\n\n const constraints = getSettingsFieldConstraints(field.key);\n if (constraints?.min != null && parsedValue < constraints.min) {\n throw new Error(`${field.label} must be at least ${constraints.min}`);\n }\n if (constraints?.max != null && parsedValue > constraints.max) {\n throw new Error(`${field.label} must be at most ${constraints.max}`);\n }\n\n return parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n let parsedValue: unknown;\n try {\n parsedValue = JSON.parse(stringValue);\n } catch {\n throw new Error(`Invalid JSON for ${field.label}`);\n }\n\n if (field.value_type === \"array\") {\n if (!Array.isArray(parsedValue)) {\n throw new Error(`${field.label} must be a JSON array`);\n }\n return parsedValue as SettingsValue[];\n }\n\n if (\n parsedValue === null ||\n Array.isArray(parsedValue) ||\n typeof parsedValue !== \"object\"\n ) {\n throw new Error(`${field.label} must be a JSON object`);\n }\n\n return parsedValue as { [key: string]: SettingsValue };\n }\n\n const stringValue = String(rawValue);\n if (stringValue === \"\" && !field.secret) {\n return null;\n }\n\n return stringValue;\n}\n\nexport function buildSdkSettingsPayload(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n): SdkSettingsPayload {\n const payload: Record<string, unknown> = {};\n\n for (const field of getSchemaFields(schema)) {\n if (dirty[field.key]) {\n setDotted(payload, field.key, coerceFieldValue(field, values[field.key]));\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\nfunction isFieldVisibleInView(\n field: SettingsFieldSchema,\n view: SettingsView,\n): boolean {\n return VIEW_PROMINENCES[view].has(field.prominence);\n}\n\nexport function buildSdkSettingsPayloadForView(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n view: SettingsView,\n): SdkSettingsPayload {\n const payload = buildSdkSettingsPayload(schema, values, dirty) as Record<\n string,\n unknown\n >;\n\n for (const field of getSchemaFields(schema)) {\n if (!isFieldVisibleInView(field, view)) {\n setDotted(payload, field.key, field.default ?? null);\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\n/** Return sections with fields filtered for the current view tier.\n * Specially-rendered fields are excluded from the generic list. */\nexport function getVisibleSettingsSections(\n schema: SettingsSchema,\n values: SettingsFormValues,\n view: SettingsView,\n excludeKeys: Set<string> = SPECIALLY_RENDERED_KEYS,\n): SettingsSectionSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections\n .map((section) => ({\n ...section,\n fields: section.fields.filter(\n (field) =>\n !excludeKeys.has(field.key) &&\n isFieldVisibleInView(field, view) &&\n isSettingsFieldVisible(field, values),\n ),\n }))\n .filter((section) => section.fields.length > 0);\n}\n\n/** Whether the schema has any fields visible in the \"advanced\" tier. */\nexport function hasAdvancedSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"major\");\n}\n\n/** Whether the schema has any \"minor\" prominence fields. */\nexport function hasMinorSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"minor\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sdk-settings-schema.cjs","names":[],"sources":["../../src/utils/sdk-settings-schema.ts"],"sourcesContent":["import {\n SettingProminence,\n Settings,\n SettingsFieldSchema,\n SettingsSchema,\n SettingsSectionSchema,\n SettingsValue,\n} from \"#/types/settings\";\nimport { getSettingsFieldConstraints } from \"#/utils/sdk-settings-field-metadata\";\n\nexport type SettingsFormValues = Record<string, string | boolean>;\nexport type SettingsDirtyState = Record<string, boolean>;\nexport type SdkSettingsPayload = Record<string, SettingsValue>;\nexport type SettingsValueSource = \"agent_settings\" | \"conversation_settings\";\n\nexport type SettingsView = \"basic\" | \"advanced\" | \"all\";\n\n/** Fields that are rendered by purpose-built components instead of the\n * generic `SchemaField` renderer. */\nexport const SPECIALLY_RENDERED_KEYS = new Set([\n \"llm.model\",\n \"llm.api_key\",\n \"llm.base_url\",\n]);\n\n/** Prominence tiers visible at each view level. */\nconst VIEW_PROMINENCES: Record<SettingsView, Set<SettingProminence>> = {\n basic: new Set<SettingProminence>([\"critical\"]),\n advanced: new Set<SettingProminence>([\"critical\", \"major\"]),\n all: new Set<SettingProminence>([\"critical\", \"major\", \"minor\"]),\n};\n\n/**\n * True when `schema` looks like a usable `SettingsSchema` — i.e. an\n * object with an array `sections` field. Guards every helper in this\n * module against malformed/empty schema responses (e.g. when the\n * frontend ends up pointing at a host that does not actually serve\n * `/api/settings/agent-schema`, such as an unconfigured Vercel preview\n * origin that returns the React Router SPA shell for arbitrary\n * `/api/*` paths). Without this check, `schema.sections.filter(...)`\n * inside `SdkSectionPage` blows up with\n * `Cannot read properties of undefined (reading 'filter')` and React\n * Router escalates to a full-screen error page.\n */\nexport function isValidSettingsSchema(\n schema: SettingsSchema | null | undefined,\n): schema is SettingsSchema {\n return !!schema && Array.isArray((schema as SettingsSchema).sections);\n}\n\nfunction getSchemaFields(schema: SettingsSchema): SettingsFieldSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections.flatMap((section) => section.fields);\n}\n\n/** Traverse a nested object using a dotted key path (e.g. \"llm.model\"). */\nfunction lookupDotted(\n obj: Record<string, unknown> | null | undefined,\n key: string,\n): unknown {\n if (!obj) return undefined;\n const parts = key.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/** Set a value in a nested object at a dotted key path (e.g. \"llm.model\"). */\nfunction setDotted(\n obj: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n const parts = key.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n\nexport function getSettingValue(\n settings: Settings,\n key: string,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsValue {\n return (lookupDotted(settings[source] as Record<string, unknown>, key) ??\n null) as SettingsValue;\n}\n\nexport function getAgentSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"agent_settings\");\n}\n\nexport function getConversationSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"conversation_settings\");\n}\n\nfunction isChoiceField(field: SettingsFieldSchema): boolean {\n return field.choices.length > 0;\n}\n\nfunction isCriticalField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"critical\";\n}\n\nfunction isMinorField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"minor\";\n}\n\nexport function normalizeFieldValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): string | boolean {\n const resolvedValue = rawValue ?? field.default;\n\n if (isChoiceField(field)) {\n return resolvedValue === null || resolvedValue === undefined\n ? \"\"\n : String(resolvedValue);\n }\n\n if (field.value_type === \"boolean\") {\n return Boolean(resolvedValue ?? false);\n }\n\n if (resolvedValue === null || resolvedValue === undefined) {\n return \"\";\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n return JSON.stringify(resolvedValue, null, 2);\n }\n\n return String(resolvedValue);\n}\n\nfunction normalizeComparableValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): boolean | number | string | null {\n if (rawValue === undefined) {\n return null;\n }\n\n if (field.value_type === \"boolean\") {\n if (typeof rawValue === \"string\") {\n if (rawValue === \"true\") {\n return true;\n }\n if (rawValue === \"false\") {\n return false;\n }\n }\n if (rawValue === null) {\n return null;\n }\n return Boolean(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n if (rawValue === \"\" || rawValue === null) {\n return null;\n }\n\n const parsedValue =\n typeof rawValue === \"number\" ? rawValue : Number(String(rawValue));\n return Number.isNaN(parsedValue) ? null : parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n if (rawValue === null) {\n return null;\n }\n\n // Treat empty objects as null so that serializer artifacts\n // (e.g. mcp_config: {} vs schema default null) don't trigger\n // a spurious view escalation in inferInitialView.\n if (\n field.value_type === \"object\" &&\n typeof rawValue === \"object\" &&\n !Array.isArray(rawValue) &&\n Object.keys(rawValue as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n\n if (typeof rawValue === \"string\") {\n const trimmedValue = rawValue.trim();\n if (!trimmedValue) {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(trimmedValue);\n // Also normalise stringified empty objects\n if (\n field.value_type === \"object\" &&\n parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed) &&\n Object.keys(parsed as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n return JSON.stringify(parsed);\n } catch {\n return trimmedValue;\n }\n }\n\n return JSON.stringify(rawValue);\n }\n\n if (rawValue === null) {\n return null;\n }\n\n return String(rawValue);\n}\n\nexport function buildInitialSettingsFormValues(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsFormValues {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return {};\n }\n\n return Object.fromEntries(\n getSchemaFields(schema).map((field) => [\n field.key,\n normalizeFieldValue(field, getSettingValue(settings, field.key, source)),\n ]),\n );\n}\n\nexport function inferInitialView(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsView {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return \"basic\";\n }\n\n let hasMinorOverride = false;\n let hasMajorOverride = false;\n\n for (const field of getSchemaFields(schema)) {\n if (!isCriticalField(field)) {\n const currentValue = getSettingValue(settings, field.key, source);\n const isDifferent =\n normalizeComparableValue(\n field,\n currentValue ?? field.default ?? null,\n ) !== normalizeComparableValue(field, field.default ?? null);\n\n if (isDifferent) {\n if (isMinorField(field)) {\n hasMinorOverride = true;\n } else {\n hasMajorOverride = true;\n }\n }\n }\n }\n\n if (hasMinorOverride) return \"all\";\n if (hasMajorOverride) return \"advanced\";\n return \"basic\";\n}\n\n/** Determine which view tier to default to based on whether the user has\n * overridden any non-critical settings. */\nexport function hasAdvancedSettingsOverrides(settings: Settings): boolean {\n return inferInitialView(settings) !== \"basic\";\n}\n\nexport function isSettingsFieldVisible(\n field: SettingsFieldSchema,\n values: SettingsFormValues,\n): boolean {\n return field.depends_on.every((dependency) => values[dependency] === true);\n}\n\nfunction parseBooleanFieldValue(rawValue: string | boolean): boolean | null {\n if (typeof rawValue === \"boolean\") {\n return rawValue;\n }\n\n const normalizedValue = rawValue.trim().toLowerCase();\n if (!normalizedValue) {\n return null;\n }\n if (normalizedValue === \"true\") {\n return true;\n }\n if (normalizedValue === \"false\") {\n return false;\n }\n\n throw new Error(`Expected a boolean value, received: ${rawValue}`);\n}\n\nfunction coerceFieldValue(\n field: SettingsFieldSchema,\n rawValue: string | boolean,\n): SettingsValue {\n if (field.value_type === \"boolean\") {\n return parseBooleanFieldValue(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n const parsedValue = Number(stringValue);\n if (Number.isNaN(parsedValue)) {\n throw new Error(`Expected a numeric value, received: ${stringValue}`);\n }\n if (field.value_type === \"integer\" && !Number.isInteger(parsedValue)) {\n throw new Error(`Expected an integer value, received: ${stringValue}`);\n }\n\n const constraints = getSettingsFieldConstraints(field.key);\n if (constraints?.min != null && parsedValue < constraints.min) {\n throw new Error(`${field.label} must be at least ${constraints.min}`);\n }\n if (constraints?.max != null && parsedValue > constraints.max) {\n throw new Error(`${field.label} must be at most ${constraints.max}`);\n }\n\n return parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n let parsedValue: unknown;\n try {\n parsedValue = JSON.parse(stringValue);\n } catch {\n throw new Error(`Invalid JSON for ${field.label}`);\n }\n\n if (field.value_type === \"array\") {\n if (!Array.isArray(parsedValue)) {\n throw new Error(`${field.label} must be a JSON array`);\n }\n return parsedValue as SettingsValue[];\n }\n\n if (\n parsedValue === null ||\n Array.isArray(parsedValue) ||\n typeof parsedValue !== \"object\"\n ) {\n throw new Error(`${field.label} must be a JSON object`);\n }\n\n return parsedValue as { [key: string]: SettingsValue };\n }\n\n const stringValue = String(rawValue);\n if (stringValue === \"\" && !field.secret) {\n return null;\n }\n\n return stringValue;\n}\n\nexport function buildSdkSettingsPayload(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n): SdkSettingsPayload {\n const payload: Record<string, unknown> = {};\n\n for (const field of getSchemaFields(schema)) {\n if (dirty[field.key]) {\n setDotted(payload, field.key, coerceFieldValue(field, values[field.key]));\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\nfunction isFieldVisibleInView(\n field: SettingsFieldSchema,\n view: SettingsView,\n): boolean {\n return VIEW_PROMINENCES[view].has(field.prominence);\n}\n\nexport function buildSdkSettingsPayloadForView(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n view: SettingsView,\n): SdkSettingsPayload {\n const payload = buildSdkSettingsPayload(schema, values, dirty) as Record<\n string,\n unknown\n >;\n\n for (const field of getSchemaFields(schema)) {\n if (!isFieldVisibleInView(field, view)) {\n setDotted(payload, field.key, field.default ?? null);\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\n/** Return sections with fields filtered for the current view tier.\n * Specially-rendered fields are excluded from the generic list. */\nexport function getVisibleSettingsSections(\n schema: SettingsSchema,\n values: SettingsFormValues,\n view: SettingsView,\n excludeKeys: Set<string> = SPECIALLY_RENDERED_KEYS,\n): SettingsSectionSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections\n .map((section) => ({\n ...section,\n fields: section.fields.filter(\n (field) =>\n !excludeKeys.has(field.key) &&\n isFieldVisibleInView(field, view) &&\n isSettingsFieldVisible(field, values),\n ),\n }))\n .filter((section) => section.fields.length > 0);\n}\n\n/** Whether the schema has any fields visible in the \"advanced\" tier. */\nexport function hasAdvancedSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"major\");\n}\n\n/** Whether the schema has any \"minor\" prominence fields. */\nexport function hasMinorSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"minor\");\n}\n"],"mappings":"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"}
@@ -23,6 +23,7 @@ export declare function isValidSettingsSchema(schema: SettingsSchema | null | un
23
23
  export declare function getSettingValue(settings: Settings, key: string, source?: SettingsValueSource): SettingsValue;
24
24
  export declare function getAgentSettingValue(settings: Settings, key: string): SettingsValue;
25
25
  export declare function getConversationSettingValue(settings: Settings, key: string): SettingsValue;
26
+ export declare function normalizeFieldValue(field: SettingsFieldSchema, rawValue: unknown): string | boolean;
26
27
  export declare function buildInitialSettingsFormValues(settings: Settings, schemaOverride?: SettingsSchema | null, source?: SettingsValueSource): SettingsFormValues;
27
28
  export declare function inferInitialView(settings: Settings, schemaOverride?: SettingsSchema | null, source?: SettingsValueSource): SettingsView;
28
29
  /** Determine which view tier to default to based on whether the user has
@@ -165,6 +165,6 @@ function w(e) {
165
165
  return e ? i(e).some((e) => e.prominence === "minor") : !1;
166
166
  }
167
167
  //#endregion
168
- export { m as buildInitialSettingsFormValues, x as buildSdkSettingsPayloadForView, c as getAgentSettingValue, S as getVisibleSettingsSections, C as hasAdvancedSettings, w as hasMinorSettings, h as inferInitialView, r as isValidSettingsSchema };
168
+ export { m as buildInitialSettingsFormValues, y as buildSdkSettingsPayload, x as buildSdkSettingsPayloadForView, c as getAgentSettingValue, S as getVisibleSettingsSections, C as hasAdvancedSettings, w as hasMinorSettings, h as inferInitialView, r as isValidSettingsSchema };
169
169
 
170
170
  //# sourceMappingURL=sdk-settings-schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sdk-settings-schema.js","names":[],"sources":["../../src/utils/sdk-settings-schema.ts"],"sourcesContent":["import {\n SettingProminence,\n Settings,\n SettingsFieldSchema,\n SettingsSchema,\n SettingsSectionSchema,\n SettingsValue,\n} from \"#/types/settings\";\nimport { getSettingsFieldConstraints } from \"#/utils/sdk-settings-field-metadata\";\n\nexport type SettingsFormValues = Record<string, string | boolean>;\nexport type SettingsDirtyState = Record<string, boolean>;\nexport type SdkSettingsPayload = Record<string, SettingsValue>;\nexport type SettingsValueSource = \"agent_settings\" | \"conversation_settings\";\n\nexport type SettingsView = \"basic\" | \"advanced\" | \"all\";\n\n/** Fields that are rendered by purpose-built components instead of the\n * generic `SchemaField` renderer. */\nexport const SPECIALLY_RENDERED_KEYS = new Set([\n \"llm.model\",\n \"llm.api_key\",\n \"llm.base_url\",\n]);\n\n/** Prominence tiers visible at each view level. */\nconst VIEW_PROMINENCES: Record<SettingsView, Set<SettingProminence>> = {\n basic: new Set<SettingProminence>([\"critical\"]),\n advanced: new Set<SettingProminence>([\"critical\", \"major\"]),\n all: new Set<SettingProminence>([\"critical\", \"major\", \"minor\"]),\n};\n\n/**\n * True when `schema` looks like a usable `SettingsSchema` — i.e. an\n * object with an array `sections` field. Guards every helper in this\n * module against malformed/empty schema responses (e.g. when the\n * frontend ends up pointing at a host that does not actually serve\n * `/api/settings/agent-schema`, such as an unconfigured Vercel preview\n * origin that returns the React Router SPA shell for arbitrary\n * `/api/*` paths). Without this check, `schema.sections.filter(...)`\n * inside `SdkSectionPage` blows up with\n * `Cannot read properties of undefined (reading 'filter')` and React\n * Router escalates to a full-screen error page.\n */\nexport function isValidSettingsSchema(\n schema: SettingsSchema | null | undefined,\n): schema is SettingsSchema {\n return !!schema && Array.isArray((schema as SettingsSchema).sections);\n}\n\nfunction getSchemaFields(schema: SettingsSchema): SettingsFieldSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections.flatMap((section) => section.fields);\n}\n\n/** Traverse a nested object using a dotted key path (e.g. \"llm.model\"). */\nfunction lookupDotted(\n obj: Record<string, unknown> | null | undefined,\n key: string,\n): unknown {\n if (!obj) return undefined;\n const parts = key.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/** Set a value in a nested object at a dotted key path (e.g. \"llm.model\"). */\nfunction setDotted(\n obj: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n const parts = key.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n\nexport function getSettingValue(\n settings: Settings,\n key: string,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsValue {\n return (lookupDotted(settings[source] as Record<string, unknown>, key) ??\n null) as SettingsValue;\n}\n\nexport function getAgentSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"agent_settings\");\n}\n\nexport function getConversationSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"conversation_settings\");\n}\n\nfunction isChoiceField(field: SettingsFieldSchema): boolean {\n return field.choices.length > 0;\n}\n\nfunction isCriticalField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"critical\";\n}\n\nfunction isMinorField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"minor\";\n}\n\nfunction normalizeFieldValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): string | boolean {\n const resolvedValue = rawValue ?? field.default;\n\n if (isChoiceField(field)) {\n return resolvedValue === null || resolvedValue === undefined\n ? \"\"\n : String(resolvedValue);\n }\n\n if (field.value_type === \"boolean\") {\n return Boolean(resolvedValue ?? false);\n }\n\n if (resolvedValue === null || resolvedValue === undefined) {\n return \"\";\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n return JSON.stringify(resolvedValue, null, 2);\n }\n\n return String(resolvedValue);\n}\n\nfunction normalizeComparableValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): boolean | number | string | null {\n if (rawValue === undefined) {\n return null;\n }\n\n if (field.value_type === \"boolean\") {\n if (typeof rawValue === \"string\") {\n if (rawValue === \"true\") {\n return true;\n }\n if (rawValue === \"false\") {\n return false;\n }\n }\n if (rawValue === null) {\n return null;\n }\n return Boolean(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n if (rawValue === \"\" || rawValue === null) {\n return null;\n }\n\n const parsedValue =\n typeof rawValue === \"number\" ? rawValue : Number(String(rawValue));\n return Number.isNaN(parsedValue) ? null : parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n if (rawValue === null) {\n return null;\n }\n\n // Treat empty objects as null so that serializer artifacts\n // (e.g. mcp_config: {} vs schema default null) don't trigger\n // a spurious view escalation in inferInitialView.\n if (\n field.value_type === \"object\" &&\n typeof rawValue === \"object\" &&\n !Array.isArray(rawValue) &&\n Object.keys(rawValue as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n\n if (typeof rawValue === \"string\") {\n const trimmedValue = rawValue.trim();\n if (!trimmedValue) {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(trimmedValue);\n // Also normalise stringified empty objects\n if (\n field.value_type === \"object\" &&\n parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed) &&\n Object.keys(parsed as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n return JSON.stringify(parsed);\n } catch {\n return trimmedValue;\n }\n }\n\n return JSON.stringify(rawValue);\n }\n\n if (rawValue === null) {\n return null;\n }\n\n return String(rawValue);\n}\n\nexport function buildInitialSettingsFormValues(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsFormValues {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return {};\n }\n\n return Object.fromEntries(\n getSchemaFields(schema).map((field) => [\n field.key,\n normalizeFieldValue(field, getSettingValue(settings, field.key, source)),\n ]),\n );\n}\n\nexport function inferInitialView(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsView {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return \"basic\";\n }\n\n let hasMinorOverride = false;\n let hasMajorOverride = false;\n\n for (const field of getSchemaFields(schema)) {\n if (!isCriticalField(field)) {\n const currentValue = getSettingValue(settings, field.key, source);\n const isDifferent =\n normalizeComparableValue(\n field,\n currentValue ?? field.default ?? null,\n ) !== normalizeComparableValue(field, field.default ?? null);\n\n if (isDifferent) {\n if (isMinorField(field)) {\n hasMinorOverride = true;\n } else {\n hasMajorOverride = true;\n }\n }\n }\n }\n\n if (hasMinorOverride) return \"all\";\n if (hasMajorOverride) return \"advanced\";\n return \"basic\";\n}\n\n/** Determine which view tier to default to based on whether the user has\n * overridden any non-critical settings. */\nexport function hasAdvancedSettingsOverrides(settings: Settings): boolean {\n return inferInitialView(settings) !== \"basic\";\n}\n\nexport function isSettingsFieldVisible(\n field: SettingsFieldSchema,\n values: SettingsFormValues,\n): boolean {\n return field.depends_on.every((dependency) => values[dependency] === true);\n}\n\nfunction parseBooleanFieldValue(rawValue: string | boolean): boolean | null {\n if (typeof rawValue === \"boolean\") {\n return rawValue;\n }\n\n const normalizedValue = rawValue.trim().toLowerCase();\n if (!normalizedValue) {\n return null;\n }\n if (normalizedValue === \"true\") {\n return true;\n }\n if (normalizedValue === \"false\") {\n return false;\n }\n\n throw new Error(`Expected a boolean value, received: ${rawValue}`);\n}\n\nfunction coerceFieldValue(\n field: SettingsFieldSchema,\n rawValue: string | boolean,\n): SettingsValue {\n if (field.value_type === \"boolean\") {\n return parseBooleanFieldValue(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n const parsedValue = Number(stringValue);\n if (Number.isNaN(parsedValue)) {\n throw new Error(`Expected a numeric value, received: ${stringValue}`);\n }\n if (field.value_type === \"integer\" && !Number.isInteger(parsedValue)) {\n throw new Error(`Expected an integer value, received: ${stringValue}`);\n }\n\n const constraints = getSettingsFieldConstraints(field.key);\n if (constraints?.min != null && parsedValue < constraints.min) {\n throw new Error(`${field.label} must be at least ${constraints.min}`);\n }\n if (constraints?.max != null && parsedValue > constraints.max) {\n throw new Error(`${field.label} must be at most ${constraints.max}`);\n }\n\n return parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n let parsedValue: unknown;\n try {\n parsedValue = JSON.parse(stringValue);\n } catch {\n throw new Error(`Invalid JSON for ${field.label}`);\n }\n\n if (field.value_type === \"array\") {\n if (!Array.isArray(parsedValue)) {\n throw new Error(`${field.label} must be a JSON array`);\n }\n return parsedValue as SettingsValue[];\n }\n\n if (\n parsedValue === null ||\n Array.isArray(parsedValue) ||\n typeof parsedValue !== \"object\"\n ) {\n throw new Error(`${field.label} must be a JSON object`);\n }\n\n return parsedValue as { [key: string]: SettingsValue };\n }\n\n const stringValue = String(rawValue);\n if (stringValue === \"\" && !field.secret) {\n return null;\n }\n\n return stringValue;\n}\n\nexport function buildSdkSettingsPayload(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n): SdkSettingsPayload {\n const payload: Record<string, unknown> = {};\n\n for (const field of getSchemaFields(schema)) {\n if (dirty[field.key]) {\n setDotted(payload, field.key, coerceFieldValue(field, values[field.key]));\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\nfunction isFieldVisibleInView(\n field: SettingsFieldSchema,\n view: SettingsView,\n): boolean {\n return VIEW_PROMINENCES[view].has(field.prominence);\n}\n\nexport function buildSdkSettingsPayloadForView(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n view: SettingsView,\n): SdkSettingsPayload {\n const payload = buildSdkSettingsPayload(schema, values, dirty) as Record<\n string,\n unknown\n >;\n\n for (const field of getSchemaFields(schema)) {\n if (!isFieldVisibleInView(field, view)) {\n setDotted(payload, field.key, field.default ?? null);\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\n/** Return sections with fields filtered for the current view tier.\n * Specially-rendered fields are excluded from the generic list. */\nexport function getVisibleSettingsSections(\n schema: SettingsSchema,\n values: SettingsFormValues,\n view: SettingsView,\n excludeKeys: Set<string> = SPECIALLY_RENDERED_KEYS,\n): SettingsSectionSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections\n .map((section) => ({\n ...section,\n fields: section.fields.filter(\n (field) =>\n !excludeKeys.has(field.key) &&\n isFieldVisibleInView(field, view) &&\n isSettingsFieldVisible(field, values),\n ),\n }))\n .filter((section) => section.fields.length > 0);\n}\n\n/** Whether the schema has any fields visible in the \"advanced\" tier. */\nexport function hasAdvancedSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"major\");\n}\n\n/** Whether the schema has any \"minor\" prominence fields. */\nexport function hasMinorSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"minor\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sdk-settings-schema.js","names":[],"sources":["../../src/utils/sdk-settings-schema.ts"],"sourcesContent":["import {\n SettingProminence,\n Settings,\n SettingsFieldSchema,\n SettingsSchema,\n SettingsSectionSchema,\n SettingsValue,\n} from \"#/types/settings\";\nimport { getSettingsFieldConstraints } from \"#/utils/sdk-settings-field-metadata\";\n\nexport type SettingsFormValues = Record<string, string | boolean>;\nexport type SettingsDirtyState = Record<string, boolean>;\nexport type SdkSettingsPayload = Record<string, SettingsValue>;\nexport type SettingsValueSource = \"agent_settings\" | \"conversation_settings\";\n\nexport type SettingsView = \"basic\" | \"advanced\" | \"all\";\n\n/** Fields that are rendered by purpose-built components instead of the\n * generic `SchemaField` renderer. */\nexport const SPECIALLY_RENDERED_KEYS = new Set([\n \"llm.model\",\n \"llm.api_key\",\n \"llm.base_url\",\n]);\n\n/** Prominence tiers visible at each view level. */\nconst VIEW_PROMINENCES: Record<SettingsView, Set<SettingProminence>> = {\n basic: new Set<SettingProminence>([\"critical\"]),\n advanced: new Set<SettingProminence>([\"critical\", \"major\"]),\n all: new Set<SettingProminence>([\"critical\", \"major\", \"minor\"]),\n};\n\n/**\n * True when `schema` looks like a usable `SettingsSchema` — i.e. an\n * object with an array `sections` field. Guards every helper in this\n * module against malformed/empty schema responses (e.g. when the\n * frontend ends up pointing at a host that does not actually serve\n * `/api/settings/agent-schema`, such as an unconfigured Vercel preview\n * origin that returns the React Router SPA shell for arbitrary\n * `/api/*` paths). Without this check, `schema.sections.filter(...)`\n * inside `SdkSectionPage` blows up with\n * `Cannot read properties of undefined (reading 'filter')` and React\n * Router escalates to a full-screen error page.\n */\nexport function isValidSettingsSchema(\n schema: SettingsSchema | null | undefined,\n): schema is SettingsSchema {\n return !!schema && Array.isArray((schema as SettingsSchema).sections);\n}\n\nfunction getSchemaFields(schema: SettingsSchema): SettingsFieldSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections.flatMap((section) => section.fields);\n}\n\n/** Traverse a nested object using a dotted key path (e.g. \"llm.model\"). */\nfunction lookupDotted(\n obj: Record<string, unknown> | null | undefined,\n key: string,\n): unknown {\n if (!obj) return undefined;\n const parts = key.split(\".\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\n/** Set a value in a nested object at a dotted key path (e.g. \"llm.model\"). */\nfunction setDotted(\n obj: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n const parts = key.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i += 1) {\n const part = parts[i];\n if (\n current[part] == null ||\n typeof current[part] !== \"object\" ||\n Array.isArray(current[part])\n ) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n\nexport function getSettingValue(\n settings: Settings,\n key: string,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsValue {\n return (lookupDotted(settings[source] as Record<string, unknown>, key) ??\n null) as SettingsValue;\n}\n\nexport function getAgentSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"agent_settings\");\n}\n\nexport function getConversationSettingValue(\n settings: Settings,\n key: string,\n): SettingsValue {\n return getSettingValue(settings, key, \"conversation_settings\");\n}\n\nfunction isChoiceField(field: SettingsFieldSchema): boolean {\n return field.choices.length > 0;\n}\n\nfunction isCriticalField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"critical\";\n}\n\nfunction isMinorField(field: SettingsFieldSchema): boolean {\n return field.prominence === \"minor\";\n}\n\nexport function normalizeFieldValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): string | boolean {\n const resolvedValue = rawValue ?? field.default;\n\n if (isChoiceField(field)) {\n return resolvedValue === null || resolvedValue === undefined\n ? \"\"\n : String(resolvedValue);\n }\n\n if (field.value_type === \"boolean\") {\n return Boolean(resolvedValue ?? false);\n }\n\n if (resolvedValue === null || resolvedValue === undefined) {\n return \"\";\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n return JSON.stringify(resolvedValue, null, 2);\n }\n\n return String(resolvedValue);\n}\n\nfunction normalizeComparableValue(\n field: SettingsFieldSchema,\n rawValue: unknown,\n): boolean | number | string | null {\n if (rawValue === undefined) {\n return null;\n }\n\n if (field.value_type === \"boolean\") {\n if (typeof rawValue === \"string\") {\n if (rawValue === \"true\") {\n return true;\n }\n if (rawValue === \"false\") {\n return false;\n }\n }\n if (rawValue === null) {\n return null;\n }\n return Boolean(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n if (rawValue === \"\" || rawValue === null) {\n return null;\n }\n\n const parsedValue =\n typeof rawValue === \"number\" ? rawValue : Number(String(rawValue));\n return Number.isNaN(parsedValue) ? null : parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n if (rawValue === null) {\n return null;\n }\n\n // Treat empty objects as null so that serializer artifacts\n // (e.g. mcp_config: {} vs schema default null) don't trigger\n // a spurious view escalation in inferInitialView.\n if (\n field.value_type === \"object\" &&\n typeof rawValue === \"object\" &&\n !Array.isArray(rawValue) &&\n Object.keys(rawValue as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n\n if (typeof rawValue === \"string\") {\n const trimmedValue = rawValue.trim();\n if (!trimmedValue) {\n return null;\n }\n try {\n const parsed: unknown = JSON.parse(trimmedValue);\n // Also normalise stringified empty objects\n if (\n field.value_type === \"object\" &&\n parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed) &&\n Object.keys(parsed as Record<string, unknown>).length === 0\n ) {\n return null;\n }\n return JSON.stringify(parsed);\n } catch {\n return trimmedValue;\n }\n }\n\n return JSON.stringify(rawValue);\n }\n\n if (rawValue === null) {\n return null;\n }\n\n return String(rawValue);\n}\n\nexport function buildInitialSettingsFormValues(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsFormValues {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return {};\n }\n\n return Object.fromEntries(\n getSchemaFields(schema).map((field) => [\n field.key,\n normalizeFieldValue(field, getSettingValue(settings, field.key, source)),\n ]),\n );\n}\n\nexport function inferInitialView(\n settings: Settings,\n schemaOverride?: SettingsSchema | null,\n source: SettingsValueSource = \"agent_settings\",\n): SettingsView {\n const schema =\n schemaOverride ??\n (source === \"conversation_settings\"\n ? settings.conversation_settings_schema\n : settings.agent_settings_schema);\n if (!schema) {\n return \"basic\";\n }\n\n let hasMinorOverride = false;\n let hasMajorOverride = false;\n\n for (const field of getSchemaFields(schema)) {\n if (!isCriticalField(field)) {\n const currentValue = getSettingValue(settings, field.key, source);\n const isDifferent =\n normalizeComparableValue(\n field,\n currentValue ?? field.default ?? null,\n ) !== normalizeComparableValue(field, field.default ?? null);\n\n if (isDifferent) {\n if (isMinorField(field)) {\n hasMinorOverride = true;\n } else {\n hasMajorOverride = true;\n }\n }\n }\n }\n\n if (hasMinorOverride) return \"all\";\n if (hasMajorOverride) return \"advanced\";\n return \"basic\";\n}\n\n/** Determine which view tier to default to based on whether the user has\n * overridden any non-critical settings. */\nexport function hasAdvancedSettingsOverrides(settings: Settings): boolean {\n return inferInitialView(settings) !== \"basic\";\n}\n\nexport function isSettingsFieldVisible(\n field: SettingsFieldSchema,\n values: SettingsFormValues,\n): boolean {\n return field.depends_on.every((dependency) => values[dependency] === true);\n}\n\nfunction parseBooleanFieldValue(rawValue: string | boolean): boolean | null {\n if (typeof rawValue === \"boolean\") {\n return rawValue;\n }\n\n const normalizedValue = rawValue.trim().toLowerCase();\n if (!normalizedValue) {\n return null;\n }\n if (normalizedValue === \"true\") {\n return true;\n }\n if (normalizedValue === \"false\") {\n return false;\n }\n\n throw new Error(`Expected a boolean value, received: ${rawValue}`);\n}\n\nfunction coerceFieldValue(\n field: SettingsFieldSchema,\n rawValue: string | boolean,\n): SettingsValue {\n if (field.value_type === \"boolean\") {\n return parseBooleanFieldValue(rawValue);\n }\n\n if (field.value_type === \"integer\" || field.value_type === \"number\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n const parsedValue = Number(stringValue);\n if (Number.isNaN(parsedValue)) {\n throw new Error(`Expected a numeric value, received: ${stringValue}`);\n }\n if (field.value_type === \"integer\" && !Number.isInteger(parsedValue)) {\n throw new Error(`Expected an integer value, received: ${stringValue}`);\n }\n\n const constraints = getSettingsFieldConstraints(field.key);\n if (constraints?.min != null && parsedValue < constraints.min) {\n throw new Error(`${field.label} must be at least ${constraints.min}`);\n }\n if (constraints?.max != null && parsedValue > constraints.max) {\n throw new Error(`${field.label} must be at most ${constraints.max}`);\n }\n\n return parsedValue;\n }\n\n if (field.value_type === \"array\" || field.value_type === \"object\") {\n const stringValue = String(rawValue).trim();\n if (!stringValue) {\n return null;\n }\n\n let parsedValue: unknown;\n try {\n parsedValue = JSON.parse(stringValue);\n } catch {\n throw new Error(`Invalid JSON for ${field.label}`);\n }\n\n if (field.value_type === \"array\") {\n if (!Array.isArray(parsedValue)) {\n throw new Error(`${field.label} must be a JSON array`);\n }\n return parsedValue as SettingsValue[];\n }\n\n if (\n parsedValue === null ||\n Array.isArray(parsedValue) ||\n typeof parsedValue !== \"object\"\n ) {\n throw new Error(`${field.label} must be a JSON object`);\n }\n\n return parsedValue as { [key: string]: SettingsValue };\n }\n\n const stringValue = String(rawValue);\n if (stringValue === \"\" && !field.secret) {\n return null;\n }\n\n return stringValue;\n}\n\nexport function buildSdkSettingsPayload(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n): SdkSettingsPayload {\n const payload: Record<string, unknown> = {};\n\n for (const field of getSchemaFields(schema)) {\n if (dirty[field.key]) {\n setDotted(payload, field.key, coerceFieldValue(field, values[field.key]));\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\nfunction isFieldVisibleInView(\n field: SettingsFieldSchema,\n view: SettingsView,\n): boolean {\n return VIEW_PROMINENCES[view].has(field.prominence);\n}\n\nexport function buildSdkSettingsPayloadForView(\n schema: SettingsSchema,\n values: SettingsFormValues,\n dirty: SettingsDirtyState,\n view: SettingsView,\n): SdkSettingsPayload {\n const payload = buildSdkSettingsPayload(schema, values, dirty) as Record<\n string,\n unknown\n >;\n\n for (const field of getSchemaFields(schema)) {\n if (!isFieldVisibleInView(field, view)) {\n setDotted(payload, field.key, field.default ?? null);\n }\n }\n\n return payload as SdkSettingsPayload;\n}\n\n/** Return sections with fields filtered for the current view tier.\n * Specially-rendered fields are excluded from the generic list. */\nexport function getVisibleSettingsSections(\n schema: SettingsSchema,\n values: SettingsFormValues,\n view: SettingsView,\n excludeKeys: Set<string> = SPECIALLY_RENDERED_KEYS,\n): SettingsSectionSchema[] {\n if (!isValidSettingsSchema(schema)) return [];\n return schema.sections\n .map((section) => ({\n ...section,\n fields: section.fields.filter(\n (field) =>\n !excludeKeys.has(field.key) &&\n isFieldVisibleInView(field, view) &&\n isSettingsFieldVisible(field, values),\n ),\n }))\n .filter((section) => section.fields.length > 0);\n}\n\n/** Whether the schema has any fields visible in the \"advanced\" tier. */\nexport function hasAdvancedSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"major\");\n}\n\n/** Whether the schema has any \"minor\" prominence fields. */\nexport function hasMinorSettings(schema: SettingsSchema | null): boolean {\n if (!schema) return false;\n return getSchemaFields(schema).some((f) => f.prominence === \"minor\");\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/agent-canvas",
3
- "version": "1.0.0-alpha.8",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Agent Canvas UI for OpenHands - run AI coding agents with a visual interface",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -23,8 +23,8 @@
23
23
  "@heroui/react": "2.8.10",
24
24
  "@microlink/react-json-view": "1.31.20",
25
25
  "@monaco-editor/react": "4.7.0",
26
- "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#39711065f53166c52608462f60a4c8507253ce56",
27
- "@openhands/typescript-client": "1.24.0",
26
+ "@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#e14f740c59b4bfd7369d4bb6aea5eeb33dd05909",
27
+ "@openhands/typescript-client": "1.24.3",
28
28
  "@react-router/node": "7.14.2",
29
29
  "@react-router/serve": "7.14.2",
30
30
  "@tailwindcss/vite": "4.2.4",
@@ -42,7 +42,6 @@ const LOCAL_AGENT_SERVER_SUBDIRS = [
42
42
  "openhands-tools",
43
43
  "openhands-workspace",
44
44
  ];
45
- const DEFAULT_SECRET_KEY = SHARED_DEFAULTS.defaults.secretKey;
46
45
  const DEFAULT_AGENT_SERVER_VERSION = SHARED_DEFAULTS.versions.agentServer;
47
46
  const FRONTEND_REQUIRED_BINS = ["cross-env", "react-router"];
48
47
 
@@ -54,20 +53,36 @@ export function generateRandomApiKey() {
54
53
  return randomBytes(32).toString("hex");
55
54
  }
56
55
 
57
- // Where the auto-generated default session API key is persisted so it stays
58
- // stable across `npm run dev` restarts. Keeping the key stable means the value
59
- // baked into the frontend (VITE_SESSION_API_KEY) and the persisted
60
- // backend-registry entry (`openhands-backends` localStorage) stay in sync
61
- // without users needing to set anything in `.env`.
56
+ // Where the auto-generated API key is persisted so it stays stable across
57
+ // `npm run dev` restarts. Keeping the key stable means the value baked into
58
+ // the frontend (VITE_SESSION_API_KEY) and the persisted backend-registry entry
59
+ // (`openhands-backends` localStorage) stay in sync without users needing to
60
+ // set anything in `.env`.
62
61
  //
63
62
  // To rotate the key, delete this file. To pin a key explicitly, export
64
- // SESSION_API_KEY (or OH_SESSION_API_KEYS_0 / VITE_SESSION_API_KEY) -- those
65
- // take precedence over the persisted file.
66
- export const DEFAULT_SESSION_API_KEY_PATH = path.join(
63
+ // LOCAL_BACKEND_API_KEY it takes precedence over the persisted file.
64
+ export const DEFAULT_API_KEY_PATH = path.join(
67
65
  homedir(),
68
66
  ".openhands",
69
67
  "agent-canvas",
70
- "session-api-key.txt",
68
+ "api-key.txt",
69
+ );
70
+
71
+ /** @deprecated Use DEFAULT_API_KEY_PATH */
72
+ export const DEFAULT_SESSION_API_KEY_PATH = DEFAULT_API_KEY_PATH;
73
+
74
+ // Where the OH_SECRET_KEY is persisted so dev mode and Docker mode share the
75
+ // same encryption key when both use ~/.openhands as their state directory.
76
+ // docker/entrypoint.sh reads and writes this same file, so whichever mode runs
77
+ // first generates the key and the other picks it up automatically.
78
+ //
79
+ // To rotate the key, delete this file and restart both modes. To pin a key
80
+ // explicitly, export OH_SECRET_KEY — that takes precedence over the file.
81
+ export const DEFAULT_SECRET_KEY_PATH = path.join(
82
+ homedir(),
83
+ ".openhands",
84
+ "agent-canvas",
85
+ "secret-key.txt",
71
86
  );
72
87
 
73
88
  // Cache so repeated lookups within a single process return the same key,
@@ -75,22 +90,29 @@ export const DEFAULT_SESSION_API_KEY_PATH = path.join(
75
90
  const persistedApiKeyCache = new Map();
76
91
 
77
92
  /**
78
- * Load the persisted default session API key, generating + persisting one if
79
- * the file doesn't exist yet.
93
+ * Load the persisted default API key, generating + persisting one if the file
94
+ * doesn't exist yet.
80
95
  *
81
96
  * Best-effort: if the file can't be written (e.g. read-only home dir), we
82
97
  * fall back to an in-memory key for this process so dev still works -- the
83
98
  * key just won't survive a restart.
84
99
  *
85
100
  * @param {string} filePath - Where to read/write the key.
86
- * @returns {string} The (hex) session API key.
101
+ * @returns {string} The (hex) API key.
87
102
  */
88
- export function getOrCreatePersistedSessionApiKey(
89
- filePath = DEFAULT_SESSION_API_KEY_PATH,
103
+ export function getOrCreatePersistedApiKeyFile(
104
+ filePath = DEFAULT_API_KEY_PATH,
90
105
  ) {
91
106
  return getOrCreatePersistedApiKey(filePath, "session");
92
107
  }
93
108
 
109
+ /** @deprecated Use getOrCreatePersistedApiKeyFile */
110
+ export function getOrCreatePersistedSessionApiKey(
111
+ filePath = DEFAULT_API_KEY_PATH,
112
+ ) {
113
+ return getOrCreatePersistedApiKeyFile(filePath);
114
+ }
115
+
94
116
  /**
95
117
  * Load a persisted default API key, generating + persisting one if the file
96
118
  * doesn't exist yet.
@@ -210,6 +232,36 @@ function tryPort(port, host = "127.0.0.1") {
210
232
  });
211
233
  }
212
234
 
235
+ /**
236
+ * Assert that all listed ports are available, throwing a descriptive error if
237
+ * any are already in use.
238
+ *
239
+ * Intended as a pre-flight check before spawning services so that a concurrent
240
+ * agent-canvas instance is detected immediately rather than silently starting
241
+ * on a different port.
242
+ *
243
+ * @param {Array<{name: string, port: number}>} portConfigs - Named port list
244
+ * @param {string} [host]
245
+ */
246
+ export async function assertPortsFree(portConfigs, host = "127.0.0.1") {
247
+ const results = await Promise.all(
248
+ portConfigs.map(async ({ name, port }) => ({
249
+ name,
250
+ port,
251
+ free: await tryPort(port, host),
252
+ })),
253
+ );
254
+ const busy = results.filter(({ free }) => !free);
255
+ if (busy.length === 0) return;
256
+
257
+ const lines = busy.map(({ name, port }) => ` • ${name}: port ${port}`).join("\n");
258
+ throw new Error(
259
+ `Cannot start: the following ports are already in use:\n\n${lines}\n\n` +
260
+ `Another agent-canvas instance may already be running.\n` +
261
+ `Stop it first, or override the port via environment variables (e.g. PORT=<other>).`,
262
+ );
263
+ }
264
+
213
265
  /**
214
266
  * Find multiple free ports at once, each preferring its specified default.
215
267
  *
@@ -491,26 +543,14 @@ export async function buildSafeDevConfigAsync(
491
543
  preferredBackendPort + 1,
492
544
  );
493
545
 
494
- // Find available ports, preferring the defaults
495
- const ports = await findFreePorts([
496
- { name: "backend", preferred: preferredBackendPort },
497
- { name: "vscode", preferred: preferredVscodePort },
546
+ // Fail fast if any required port is already in use.
547
+ await assertPortsFree([
548
+ { name: "agent-server", port: preferredBackendPort },
549
+ { name: "vscode", port: preferredVscodePort },
498
550
  ]);
499
551
 
500
- // Log if we're using non-default ports
501
- if (ports.backend !== preferredBackendPort) {
502
- console.log(
503
- ` ℹ Port ${preferredBackendPort} busy, using ${ports.backend} for agent-server`,
504
- );
505
- }
506
- if (ports.vscode !== preferredVscodePort) {
507
- console.log(
508
- ` ℹ Port ${preferredVscodePort} busy, using ${ports.vscode} for vscode`,
509
- );
510
- }
511
-
512
552
  return buildConfigFromPorts(
513
- { backendPort: ports.backend, vscodePort: ports.vscode },
553
+ { backendPort: preferredBackendPort, vscodePort: preferredVscodePort },
514
554
  cwd,
515
555
  env,
516
556
  );
@@ -550,26 +590,26 @@ function buildConfigFromPorts(ports, cwd, env) {
550
590
  );
551
591
  const conversationsPath = path.join(stateDir, "conversations");
552
592
  const workspacesPath = path.join(stateDir, "workspaces");
553
- // Use provided secret key or default for local development
554
- const secretKey = env.OH_SECRET_KEY || DEFAULT_SECRET_KEY;
555
- // Use provided session API key or fall back to a key persisted to
556
- // ~/.openhands/agent-canvas/session-api-key.txt. Persisting on disk keeps
557
- // the agent-server, the Vite-baked VITE_SESSION_API_KEY, and any
593
+ // Use provided secret key, or read/generate one persisted to
594
+ // ~/.openhands/agent-canvas/secret-key.txt. Persisting ensures dev mode
595
+ // and Docker mode share the same encryption key when they mount the same
596
+ // ~/.openhands directory (docker/entrypoint.sh reads/writes the same file).
597
+ const secretKeyPath =
598
+ env.OH_SECRET_KEY_PATH || DEFAULT_SECRET_KEY_PATH;
599
+ const secretKey =
600
+ env.OH_SECRET_KEY || getOrCreatePersistedApiKey(secretKeyPath, "secret");
601
+ // Use the user-provided LOCAL_BACKEND_API_KEY or fall back to a key
602
+ // persisted to ~/.openhands/agent-canvas/api-key.txt. Persisting on disk
603
+ // keeps the agent-server, the Vite-baked VITE_SESSION_API_KEY, and any
558
604
  // `openhands-backends` localStorage entries the frontend has cached all
559
605
  // pointing at the same value across dev restarts.
560
606
  //
561
- // Check multiple env vars that may be used:
562
- // - SESSION_API_KEY: Common name
563
- // - OH_SESSION_API_KEYS_0: Used by agent-server V1 config
564
- // - VITE_SESSION_API_KEY: Used by frontend config
607
+ // LOCAL_BACKEND_API_KEY is the single user-facing env var for the API key.
565
608
  // OH_SESSION_API_KEY_PATH overrides the persisted file path (used by tests).
566
- const persistedKeyPath =
567
- env.OH_SESSION_API_KEY_PATH || DEFAULT_SESSION_API_KEY_PATH;
609
+ const persistedKeyPath = env.OH_SESSION_API_KEY_PATH || DEFAULT_API_KEY_PATH;
568
610
  const sessionApiKey =
569
- env.SESSION_API_KEY ||
570
- env.OH_SESSION_API_KEYS_0 ||
571
- env.VITE_SESSION_API_KEY ||
572
- getOrCreatePersistedSessionApiKey(persistedKeyPath);
611
+ env.LOCAL_BACKEND_API_KEY ||
612
+ getOrCreatePersistedApiKeyFile(persistedKeyPath);
573
613
 
574
614
  // Host directory containing Agent-Canvas-specific Python tools (e.g. the
575
615
  // canvas_ui tool). Added to OH_EXTRA_PYTHON_PATH below so the agent-server
@@ -881,16 +921,13 @@ async function main() {
881
921
 
882
922
  const secretKeySource = process.env.OH_SECRET_KEY
883
923
  ? "custom (from OH_SECRET_KEY)"
884
- : "default (for local development)";
885
-
886
- const sessionKeySource =
887
- process.env.SESSION_API_KEY ||
888
- process.env.OH_SESSION_API_KEYS_0 ||
889
- process.env.VITE_SESSION_API_KEY
890
- ? "custom (from env)"
891
- : `persisted (${
892
- process.env.OH_SESSION_API_KEY_PATH || DEFAULT_SESSION_API_KEY_PATH
893
- })`;
924
+ : `persisted (${process.env.OH_SECRET_KEY_PATH || DEFAULT_SECRET_KEY_PATH})`;
925
+
926
+ const sessionKeySource = process.env.LOCAL_BACKEND_API_KEY
927
+ ? "custom (from LOCAL_BACKEND_API_KEY)"
928
+ : `persisted (${
929
+ process.env.OH_SESSION_API_KEY_PATH || DEFAULT_API_KEY_PATH
930
+ })`;
894
931
 
895
932
  console.log(`- agent-server: ${agentServerCmd.source}`);
896
933
  console.log(`- backend: ${config.backendBaseUrl}`);
@@ -388,9 +388,8 @@ function startStaticServer(config) {
388
388
  "0.0.0.0",
389
389
  "--port",
390
390
  String(config.vitePort),
391
- // Inject the runtime session key so the pre-built frontend can
392
- // authenticate to agent-server without VITE_SESSION_API_KEY being baked
393
- // into the bundle at publish time.
391
+ // Inject the API key so the pre-built frontend can authenticate
392
+ // to the agent-server without a baked-in VITE_SESSION_API_KEY.
394
393
  ...(config.sessionApiKey
395
394
  ? ["--session-api-key", config.sessionApiKey]
396
395
  : []),