@theoremjs/studio 0.0.0-stage → 0.3.2

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 (473) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -2
  3. package/agreement.d.ts +14 -0
  4. package/agreement.js +80 -0
  5. package/asks.d.ts +47 -0
  6. package/asks.js +54 -0
  7. package/browser-connection.d.ts +49 -0
  8. package/browser-connection.js +215 -0
  9. package/browser-engine.d.ts +11 -0
  10. package/browser-engine.js +11 -0
  11. package/browser-execution.d.ts +6 -0
  12. package/browser-execution.js +18 -0
  13. package/browser-live.d.ts +4 -0
  14. package/browser-live.js +132 -0
  15. package/browser-transport.d.ts +33 -0
  16. package/browser-transport.js +229 -0
  17. package/browser.d.ts +4 -0
  18. package/browser.js +4 -0
  19. package/canonical.d.ts +10 -0
  20. package/canonical.js +25 -0
  21. package/compile-workspace.d.ts +28 -0
  22. package/compile-workspace.js +295 -0
  23. package/compile.d.ts +65 -0
  24. package/compile.js +1605 -0
  25. package/concierge-demo.d.ts +17 -0
  26. package/concierge-demo.js +1230 -0
  27. package/conversation-key.d.ts +5 -0
  28. package/conversation-key.js +10 -0
  29. package/decision-example-images.d.ts +30 -0
  30. package/decision-example-images.js +30 -0
  31. package/deno.json +3 -0
  32. package/draft.d.ts +407 -0
  33. package/draft.js +505 -0
  34. package/editors.d.ts +29 -0
  35. package/editors.js +51 -0
  36. package/example-embedding-files.d.ts +11 -0
  37. package/example-embedding-files.js +20 -0
  38. package/example.d.ts +36 -0
  39. package/example.js +357 -0
  40. package/guardrail-probe.d.ts +100 -0
  41. package/guardrail-probe.js +722 -0
  42. package/held-credentials.d.ts +28 -0
  43. package/held-credentials.js +87 -0
  44. package/installed.mjs +40 -0
  45. package/lent-keys.d.ts +122 -0
  46. package/lent-keys.js +47 -0
  47. package/line-diff.d.ts +15 -0
  48. package/line-diff.js +41 -0
  49. package/live-connection.d.ts +22 -0
  50. package/live-connection.js +27 -0
  51. package/live-session-bridge.d.ts +15 -0
  52. package/live-session-bridge.js +202 -0
  53. package/map-line.d.ts +19 -0
  54. package/map-line.js +42 -0
  55. package/map.d.ts +154 -0
  56. package/map.js +525 -0
  57. package/media-accept.d.ts +19 -0
  58. package/media-accept.js +65 -0
  59. package/mod.d.ts +39 -0
  60. package/mod.js +33 -0
  61. package/origins.d.ts +52 -0
  62. package/origins.js +165 -0
  63. package/package.json +85 -4
  64. package/page/assets/Banner-BISz3nXA.js +2 -0
  65. package/page/assets/BottomSheet-BwfBDtje.js +1 -0
  66. package/page/assets/Card-Lp-0P5eL.js +1 -0
  67. package/page/assets/CardWall-CBRLdDZW.js +1 -0
  68. package/page/assets/Center-nHS1JSux.js +1 -0
  69. package/page/assets/CheckboxInput-Cj5pk-KI.js +2 -0
  70. package/page/assets/CodeBlock-C7XJ4ioE.js +51 -0
  71. package/page/assets/CollapsibleGroup-N7_SOjbm.js +2 -0
  72. package/page/assets/ContextMenu-CBu9H-9H.js +2 -0
  73. package/page/assets/DateInput-BGrQ5tPC.js +1 -0
  74. package/page/assets/Dialog-DELWQo-v.js +1 -0
  75. package/page/assets/EmptyState-C8W4JKBZ.js +1 -0
  76. package/page/assets/HoverCard-D1Y1ltWQ.js +1 -0
  77. package/page/assets/Icon-JrysT8aU.js +3 -0
  78. package/page/assets/IconAlertTriangle-BqAEqqdU.js +1 -0
  79. package/page/assets/IconButton-DlOGzJIX.js +1 -0
  80. package/page/assets/IconEye-BB5aY-wQ.js +1 -0
  81. package/page/assets/IconFileText-6zOFKKrF.js +1 -0
  82. package/page/assets/IconSearch-BTx66a40.js +1 -0
  83. package/page/assets/IconShield-DDOh7Gr5.js +1 -0
  84. package/page/assets/ItemSwipeLayer-Ckn4X6h9.js +1 -0
  85. package/page/assets/PaneState-BvA2tLQg.js +1 -0
  86. package/page/assets/PaneState-eUzUkNE8.css +1 -0
  87. package/page/assets/ScrollableArea-Ckugdj1N.js +1 -0
  88. package/page/assets/Section-DJ97F7QE.js +1 -0
  89. package/page/assets/SegmentedControlItem-h-aaXzyS.js +1 -0
  90. package/page/assets/SidePanel-BL32abk8.js +1 -0
  91. package/page/assets/StackItem-CQviqAfM.js +1 -0
  92. package/page/assets/Text-bum6zWWx.js +2 -0
  93. package/page/assets/TimestampHoverCard-DQIG0RZF.js +1 -0
  94. package/page/assets/ToolResultCharts-CgshSv9Q.js +1 -0
  95. package/page/assets/Tooltip-BH9xiRMl.js +1 -0
  96. package/page/assets/TraceInspector-_yZiz05W.js +1 -0
  97. package/page/assets/TraceOverview-BPO-HIYK.js +57 -0
  98. package/page/assets/VStack-CkVMcNiK.js +1 -0
  99. package/page/assets/_virtual_studio-type-sources-ByxqQaOG.js +12936 -0
  100. package/page/assets/arrive-DvbqpeRS.js +1 -0
  101. package/page/assets/browser-engine-CYq9DQAM.js +9 -0
  102. package/page/assets/browser-transport-D-2jDYe7.js +47 -0
  103. package/page/assets/codicon-DF1abBS2.ttf +0 -0
  104. package/page/assets/compile-egress-BSxOdX4K.js +23 -0
  105. package/page/assets/container.stylex-BmOi-8DQ.js +1 -0
  106. package/page/assets/createReactComponent-CZwsv6DD.js +1 -0
  107. package/page/assets/decision-example-images-CZGr7YIT.js +1 -0
  108. package/page/assets/devWarning-DxuplmRl.js +1 -0
  109. package/page/assets/editor-DjpsE8to.css +1 -0
  110. package/page/assets/editor.api-Cd3ViPQa.js +908 -0
  111. package/page/assets/editor.worker-6GY8RhDc.js +1 -0
  112. package/page/assets/editor.worker-BmdWdrHZ.js +33 -0
  113. package/page/assets/editors-BJX_S59A.js +1 -0
  114. package/page/assets/egress-automata-B5ZGE1mT.js +1 -0
  115. package/page/assets/embedding-space-view-CfQBPDxD.js +140 -0
  116. package/page/assets/encode-files-Bc3HJyAh.js +1 -0
  117. package/page/assets/export-agent-C3yyzXH8.js +43 -0
  118. package/page/assets/figtree-latin-400-normal-BD4aNku5.woff +0 -0
  119. package/page/assets/figtree-latin-400-normal-g7Dtegnw.woff2 +0 -0
  120. package/page/assets/figtree-latin-500-normal-BWnGEVsr.woff2 +0 -0
  121. package/page/assets/figtree-latin-500-normal-B_yy1phn.woff +0 -0
  122. package/page/assets/figtree-latin-600-normal-BM_oTo4n.woff +0 -0
  123. package/page/assets/figtree-latin-600-normal-Cv_xCTDl.woff2 +0 -0
  124. package/page/assets/figtree-latin-700-normal-BCCt2T1q.woff +0 -0
  125. package/page/assets/figtree-latin-700-normal-th6qEP7c.woff2 +0 -0
  126. package/page/assets/focusOutline.stylex-BT0kfa4t.js +1 -0
  127. package/page/assets/focusableSelector-DXZeMU85.js +1 -0
  128. package/page/assets/groupStyles-C7Ter7zC.js +1 -0
  129. package/page/assets/guardrail-tester-Gut5xK4a.js +9 -0
  130. package/page/assets/host-patterns-C9RMP16Q.js +1 -0
  131. package/page/assets/index-CvvmFKgS.js +105 -0
  132. package/page/assets/index-DOaHlqBo.css +1 -0
  133. package/page/assets/isRtlElement-BG33-k_H.js +1 -0
  134. package/page/assets/labels-provider-4TIWDvsE.js +3 -0
  135. package/page/assets/lent-keys-C32ems-I.js +1 -0
  136. package/page/assets/live-ingress-lDUGPwth.js +1 -0
  137. package/page/assets/live-messages-CnkL4GWi.js +1 -0
  138. package/page/assets/mergeProps-ptS-gIMz.js +1 -0
  139. package/page/assets/padding.stylex-B76907Zf.js +1 -0
  140. package/page/assets/preload-helper-DpV6A1kQ.js +1 -0
  141. package/page/assets/reader-BOCCYaRp.js +7 -0
  142. package/page/assets/register-CDgivxvX.js +2 -0
  143. package/page/assets/rolldown-runtime-hePW80VL.js +1 -0
  144. package/page/assets/schemas-BQ9Jydzb.js +56 -0
  145. package/page/assets/shaped-data-BcarWqd5.js +16 -0
  146. package/page/assets/sharedResizeObserver-4U9Xq5Oq.js +1 -0
  147. package/page/assets/stub-DhdcU_Ew.js +471 -0
  148. package/page/assets/studio-code-pane-C_SAbcOa.js +8 -0
  149. package/page/assets/studio-console-DZwlEsvr.js +3 -0
  150. package/page/assets/studio-decision-B0FnCAXA.js +1 -0
  151. package/page/assets/studio-embedding-CmA51HVo.js +2 -0
  152. package/page/assets/studio-host-console-By7hqu2l.js +2 -0
  153. package/page/assets/studio-live-BrcVQcyV.js +1 -0
  154. package/page/assets/studio-map-B_xSia-z.js +1 -0
  155. package/page/assets/studio-map-Bb06Tw1F.css +1 -0
  156. package/page/assets/studio-monaco-CbKf90i6.css +1 -0
  157. package/page/assets/studio-monaco-xfyfezQS.js +34 -0
  158. package/page/assets/studio-ts.worker-B-EXG4Sv.js +67741 -0
  159. package/page/assets/studio-ts.worker-BXtW3z-O.js +1 -0
  160. package/page/assets/studio-type-checker-BOvxudki.js +23 -0
  161. package/page/assets/themeProps-3IqahpCr.js +1 -0
  162. package/page/assets/tokens.stylex-DkHf0P9C.js +1 -0
  163. package/page/assets/tool-schema-idaOe2nI.js +18 -0
  164. package/page/assets/trace-patterns-DnYJa0nI.js +1 -0
  165. package/page/assets/trace-record-C3zNksA_.js +1 -0
  166. package/page/assets/trace-schema-9CdEyG6Q.js +1 -0
  167. package/page/assets/tree-DqyAMsbI.js +3 -0
  168. package/page/assets/turn-client-Du3Um3Mg.js +2 -0
  169. package/page/assets/types-BhfXQCLx.js +1 -0
  170. package/page/assets/typescript-20GgZ8PJ.js +1 -0
  171. package/page/assets/use-one-call-Hcs8YBp1.js +1 -0
  172. package/page/assets/useAdaptivePresentation-BzmPCddb.js +1 -0
  173. package/page/assets/useClipboard-BgGRxRMJ.js +1 -0
  174. package/page/assets/useContainerReveal-BFy8N_QU.js +1 -0
  175. package/page/assets/useDevWarning-Z9SoixOP.js +1 -0
  176. package/page/assets/useLayerDismissal-B_G7IWpC.js +1 -0
  177. package/page/assets/useMediaQuery-BCT0NUeZ.js +1 -0
  178. package/page/assets/useScrollLock-Dyf6PHJt.js +1 -0
  179. package/page/assets/useScrollableArea-0NzdiBCq.js +1 -0
  180. package/page/assets/useTooltip-CJM_xhLB.js +1 -0
  181. package/page/assets/workspace-DZALqEDX.js +1 -0
  182. package/page/assets/zip-DTcEfps6.js +1 -0
  183. package/page/index.html +90 -0
  184. package/page.mjs +49 -0
  185. package/people.d.ts +87 -0
  186. package/people.js +110 -0
  187. package/policy.d.ts +43 -0
  188. package/policy.js +102 -0
  189. package/ports.d.mts +32 -0
  190. package/ports.mjs +85 -0
  191. package/probe-battery.d.ts +13 -0
  192. package/probe-battery.js +404 -0
  193. package/providers.d.ts +149 -0
  194. package/providers.js +306 -0
  195. package/read-source.d.ts +48 -0
  196. package/read-source.js +1301 -0
  197. package/read-steps.d.ts +23 -0
  198. package/read-steps.js +2 -0
  199. package/reader.d.ts +11 -0
  200. package/reader.js +11 -0
  201. package/registrations.d.ts +83 -0
  202. package/registrations.js +1 -0
  203. package/requirements.d.ts +22 -0
  204. package/requirements.js +38 -0
  205. package/run-payload-core.d.ts +28 -0
  206. package/run-payload-core.js +123 -0
  207. package/run-payload.d.ts +32 -0
  208. package/run-payload.js +26 -0
  209. package/runtime-scope.d.ts +84 -0
  210. package/runtime-scope.js +308 -0
  211. package/runtime.d.ts +84 -0
  212. package/runtime.js +387 -0
  213. package/section-notes.d.ts +33 -0
  214. package/section-notes.js +38 -0
  215. package/serve-page.mjs +96 -0
  216. package/server/draft-decide.d.ts +72 -0
  217. package/server/draft-decide.js +111 -0
  218. package/server/draft-embedding.d.ts +3 -0
  219. package/server/draft-embedding.js +35 -0
  220. package/server/draft-handler.d.ts +82 -0
  221. package/server/draft-handler.js +213 -0
  222. package/server/draft-probe.d.ts +14 -0
  223. package/server/draft-probe.js +61 -0
  224. package/server/draft-replies.d.ts +22 -0
  225. package/server/draft-replies.js +46 -0
  226. package/server/edited-session.d.ts +28 -0
  227. package/server/edited-session.js +83 -0
  228. package/server/edits.d.ts +38 -0
  229. package/server/edits.js +195 -0
  230. package/server/handler.d.ts +133 -0
  231. package/server/handler.js +598 -0
  232. package/server/lent-draft.d.ts +19 -0
  233. package/server/lent-draft.js +23 -0
  234. package/server/live.d.ts +15 -0
  235. package/server/live.js +96 -0
  236. package/server/open-editor.d.ts +63 -0
  237. package/server/open-editor.js +115 -0
  238. package/server/origins.d.ts +18 -0
  239. package/server/origins.js +330 -0
  240. package/server/project-source.d.ts +208 -0
  241. package/server/project-source.js +1042 -0
  242. package/server/project.d.ts +22 -0
  243. package/server/project.js +56 -0
  244. package/server/run-traces.d.ts +20 -0
  245. package/server/run-traces.js +96 -0
  246. package/server/save-new.d.ts +29 -0
  247. package/server/save-new.js +514 -0
  248. package/server/save-plan.d.ts +46 -0
  249. package/server/save-plan.js +1134 -0
  250. package/server/save-remove.d.ts +21 -0
  251. package/server/save-remove.js +528 -0
  252. package/server/save-session.d.ts +96 -0
  253. package/server/save-session.js +418 -0
  254. package/server/save-wire.d.ts +235 -0
  255. package/server/save-wire.js +8 -0
  256. package/server/save.d.ts +52 -0
  257. package/server/save.js +209 -0
  258. package/server/serve.d.ts +23 -0
  259. package/server/serve.js +413 -0
  260. package/server/test-connection.d.ts +17 -0
  261. package/server/test-connection.js +362 -0
  262. package/server/test-key.d.ts +2 -0
  263. package/server/test-key.js +83 -0
  264. package/server/watch.d.ts +28 -0
  265. package/server/watch.js +93 -0
  266. package/server/zod-edit.d.ts +74 -0
  267. package/server/zod-edit.js +558 -0
  268. package/server.d.ts +8 -0
  269. package/server.js +8 -0
  270. package/setups.mjs +170 -0
  271. package/shared-carry.d.ts +114 -0
  272. package/shared-carry.js +523 -0
  273. package/shared-settings.d.ts +34 -0
  274. package/shared-settings.js +70 -0
  275. package/source.d.ts +75 -0
  276. package/source.js +414 -0
  277. package/start.mjs +169 -0
  278. package/stream-lines.d.ts +9 -0
  279. package/stream-lines.js +27 -0
  280. package/stub.d.ts +6 -0
  281. package/stub.js +33 -0
  282. package/surface.d.ts +53 -0
  283. package/surface.js +631 -0
  284. package/system-markup.d.ts +20 -0
  285. package/system-markup.js +51 -0
  286. package/tool-schema.d.ts +23 -0
  287. package/tool-schema.js +69 -0
  288. package/tools.d.ts +16 -0
  289. package/tools.js +91 -0
  290. package/traces.d.ts +24 -0
  291. package/traces.js +29 -0
  292. package/transport.d.ts +57 -0
  293. package/transport.js +187 -0
  294. package/tree.d.ts +32 -0
  295. package/tree.js +83 -0
  296. package/types.d.ts +62 -0
  297. package/types.js +6 -0
  298. package/ui/acting-as-button.d.ts +2 -0
  299. package/ui/acting-as-button.js +16 -0
  300. package/ui/add-provider.d.ts +13 -0
  301. package/ui/add-provider.js +20 -0
  302. package/ui/boot-mark.css +53 -0
  303. package/ui/boot-mark.d.ts +19 -0
  304. package/ui/boot-mark.js +128 -0
  305. package/ui/boot-paint.d.ts +8 -0
  306. package/ui/boot-paint.js +8 -0
  307. package/ui/brand-icons.d.ts +57 -0
  308. package/ui/brand-icons.js +148 -0
  309. package/ui/built/theme.css +830 -0
  310. package/ui/built/theme.css.d.ts +9 -0
  311. package/ui/built/theorem-studio.d.ts +13 -0
  312. package/ui/built/theorem-studio.js +705 -0
  313. package/ui/built/theorem-studio.variants.d.ts +28 -0
  314. package/ui/code/monaco-contributions.d.ts +45 -0
  315. package/ui/code/studio-code.d.ts +7 -0
  316. package/ui/code/studio-code.js +253 -0
  317. package/ui/code/studio-monaco.d.ts +44 -0
  318. package/ui/code/studio-monaco.js +48 -0
  319. package/ui/code/studio-ts.worker.d.ts +1 -0
  320. package/ui/code/studio-ts.worker.js +16 -0
  321. package/ui/code/studio-type-checker.d.ts +12 -0
  322. package/ui/code/studio-type-checker.js +308 -0
  323. package/ui/code/studio-type-sources.d.ts +26 -0
  324. package/ui/confirm-write.d.ts +12 -0
  325. package/ui/confirm-write.js +45 -0
  326. package/ui/diff-block.d.ts +21 -0
  327. package/ui/diff-block.js +17 -0
  328. package/ui/figtree.css +59 -0
  329. package/ui/file-place.d.ts +4 -0
  330. package/ui/file-place.js +26 -0
  331. package/ui/guardrail-tester.d.ts +18 -0
  332. package/ui/guardrail-tester.js +248 -0
  333. package/ui/inspector-context.d.ts +119 -0
  334. package/ui/inspector-context.js +84 -0
  335. package/ui/inspector.d.ts +247 -0
  336. package/ui/inspector.js +284 -0
  337. package/ui/lib/acting-as.d.ts +25 -0
  338. package/ui/lib/acting-as.js +90 -0
  339. package/ui/lib/boot-sequence.d.ts +32 -0
  340. package/ui/lib/boot-sequence.js +57 -0
  341. package/ui/lib/export-agent.d.ts +11 -0
  342. package/ui/lib/export-agent.js +335 -0
  343. package/ui/lib/facet-icons.d.ts +20 -0
  344. package/ui/lib/facet-icons.js +21 -0
  345. package/ui/lib/panel-motion.d.ts +8 -0
  346. package/ui/lib/panel-motion.js +74 -0
  347. package/ui/lib/project-address.d.ts +14 -0
  348. package/ui/lib/project-address.js +24 -0
  349. package/ui/lib/project-starting.d.ts +13 -0
  350. package/ui/lib/project-starting.js +30 -0
  351. package/ui/lib/project-traces.d.ts +17 -0
  352. package/ui/lib/project-traces.js +54 -0
  353. package/ui/lib/read-map.d.ts +30 -0
  354. package/ui/lib/read-map.js +173 -0
  355. package/ui/lib/read-progress.d.ts +14 -0
  356. package/ui/lib/read-progress.js +12 -0
  357. package/ui/lib/refusal-lines.d.ts +3 -0
  358. package/ui/lib/refusal-lines.js +8 -0
  359. package/ui/lib/studio-activity.d.ts +5 -0
  360. package/ui/lib/studio-activity.js +17 -0
  361. package/ui/lib/studio-console.d.ts +27 -0
  362. package/ui/lib/studio-console.js +9 -0
  363. package/ui/lib/studio-conversation.d.ts +20 -0
  364. package/ui/lib/studio-conversation.js +130 -0
  365. package/ui/lib/studio-examples.d.ts +31 -0
  366. package/ui/lib/studio-examples.js +25 -0
  367. package/ui/lib/studio-labels.d.ts +6 -0
  368. package/ui/lib/studio-labels.js +13 -0
  369. package/ui/lib/studio-map-kept.d.ts +17 -0
  370. package/ui/lib/studio-map-kept.js +48 -0
  371. package/ui/lib/studio-open.d.ts +22 -0
  372. package/ui/lib/studio-open.js +54 -0
  373. package/ui/lib/studio-page.d.ts +33 -0
  374. package/ui/lib/studio-page.js +121 -0
  375. package/ui/lib/studio-prefs.d.ts +26 -0
  376. package/ui/lib/studio-prefs.js +87 -0
  377. package/ui/lib/studio-project.d.ts +106 -0
  378. package/ui/lib/studio-project.js +177 -0
  379. package/ui/lib/studio-restore.d.ts +49 -0
  380. package/ui/lib/studio-restore.js +251 -0
  381. package/ui/lib/studio-run-open.d.ts +15 -0
  382. package/ui/lib/studio-run-open.js +16 -0
  383. package/ui/lib/studio-session.d.ts +34 -0
  384. package/ui/lib/studio-session.js +17 -0
  385. package/ui/lib/studio-settings.d.ts +15 -0
  386. package/ui/lib/studio-settings.js +48 -0
  387. package/ui/lib/studio-store.d.ts +64 -0
  388. package/ui/lib/studio-store.js +391 -0
  389. package/ui/lib/studio-watch.d.ts +12 -0
  390. package/ui/lib/studio-watch.js +27 -0
  391. package/ui/lib/tab-fills.d.ts +6 -0
  392. package/ui/lib/tab-fills.js +14 -0
  393. package/ui/lib/tool-credentials.d.ts +3 -0
  394. package/ui/lib/tool-credentials.js +23 -0
  395. package/ui/lib/tool-probe.d.ts +19 -0
  396. package/ui/lib/tool-probe.js +104 -0
  397. package/ui/lib/type-icons.d.ts +35 -0
  398. package/ui/lib/type-icons.js +38 -0
  399. package/ui/lib/when-idle.d.ts +5 -0
  400. package/ui/lib/when-idle.js +14 -0
  401. package/ui/lib/zip.d.ts +5 -0
  402. package/ui/lib/zip.js +8 -0
  403. package/ui/map-token.css +11 -0
  404. package/ui/map-token.d.ts +17 -0
  405. package/ui/map-token.js +29 -0
  406. package/ui/mcp-icon.d.ts +6 -0
  407. package/ui/mcp-icon.js +8 -0
  408. package/ui/motion.css +113 -0
  409. package/ui/pan.d.ts +32 -0
  410. package/ui/pan.js +90 -0
  411. package/ui/profile-editor.d.ts +33 -0
  412. package/ui/profile-editor.js +2638 -0
  413. package/ui/provider-editor.d.ts +15 -0
  414. package/ui/provider-editor.js +92 -0
  415. package/ui/read-map.css +48 -0
  416. package/ui/read-map.d.ts +7 -0
  417. package/ui/read-map.js +163 -0
  418. package/ui/refusal-detail.d.ts +4 -0
  419. package/ui/refusal-detail.js +21 -0
  420. package/ui/shared-change.d.ts +9 -0
  421. package/ui/shared-change.js +28 -0
  422. package/ui/shell-motion.css +121 -0
  423. package/ui/shell-motion.d.ts +73 -0
  424. package/ui/shell-motion.js +194 -0
  425. package/ui/studio-code-pane.d.ts +19 -0
  426. package/ui/studio-code-pane.js +44 -0
  427. package/ui/studio-compare.d.ts +80 -0
  428. package/ui/studio-compare.js +326 -0
  429. package/ui/studio-conflicts.d.ts +17 -0
  430. package/ui/studio-conflicts.js +87 -0
  431. package/ui/studio-connection.d.ts +64 -0
  432. package/ui/studio-connection.js +286 -0
  433. package/ui/studio-decision.d.ts +6 -0
  434. package/ui/studio-decision.js +49 -0
  435. package/ui/studio-embedding.d.ts +6 -0
  436. package/ui/studio-embedding.js +30 -0
  437. package/ui/studio-host-console.d.ts +6 -0
  438. package/ui/studio-host-console.js +17 -0
  439. package/ui/studio-host.d.ts +73 -0
  440. package/ui/studio-host.js +12 -0
  441. package/ui/studio-live.d.ts +17 -0
  442. package/ui/studio-live.js +26 -0
  443. package/ui/studio-map.css +273 -0
  444. package/ui/studio-map.d.ts +17 -0
  445. package/ui/studio-map.js +397 -0
  446. package/ui/studio-nav.d.ts +24 -0
  447. package/ui/studio-nav.js +17 -0
  448. package/ui/studio-no-project.d.ts +5 -0
  449. package/ui/studio-no-project.js +9 -0
  450. package/ui/studio-run.css +76 -0
  451. package/ui/studio-run.d.ts +14 -0
  452. package/ui/studio-run.js +119 -0
  453. package/ui/studio-runner.d.ts +45 -0
  454. package/ui/studio-runner.js +187 -0
  455. package/ui/studio-save.d.ts +49 -0
  456. package/ui/studio-save.js +307 -0
  457. package/ui/studio-screen.d.ts +61 -0
  458. package/ui/studio-screen.js +2078 -0
  459. package/ui/studio-settings.d.ts +31 -0
  460. package/ui/studio-settings.js +288 -0
  461. package/ui/studio-shell.css +27 -0
  462. package/ui/studio-shell.d.ts +43 -0
  463. package/ui/studio-shell.js +87 -0
  464. package/ui/studio-theme.d.ts +7 -0
  465. package/ui/studio-theme.js +94 -0
  466. package/ui/studio.css +427 -0
  467. package/ui/theorem-mark.css +84 -0
  468. package/ui/theorem-mark.d.ts +22 -0
  469. package/ui/theorem-mark.js +21 -0
  470. package/vite.d.mts +30 -0
  471. package/vite.mjs +277 -0
  472. package/workspace.d.ts +230 -0
  473. package/workspace.js +1054 -0
@@ -0,0 +1,2638 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge } from '@astryxdesign/core/Badge';
3
+ import { Banner } from '@astryxdesign/core/Banner';
4
+ import { Button } from '@astryxdesign/core/Button';
5
+ import { CheckboxInput } from '@astryxdesign/core/CheckboxInput';
6
+ import { CheckboxList, CheckboxListItem } from '@astryxdesign/core/CheckboxList';
7
+ import { CodeBlock } from '@astryxdesign/core/CodeBlock';
8
+ import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible';
9
+ import { ComplexSelector } from '@astryxdesign/core/ComplexSelector';
10
+ import { FileInput } from '@astryxdesign/core/FileInput';
11
+ import { HStack } from '@astryxdesign/core/HStack';
12
+ import { Icon } from '@astryxdesign/core/Icon';
13
+ import { IconButton } from '@astryxdesign/core/IconButton';
14
+ import { List, ListItem } from '@astryxdesign/core/List';
15
+ import { Section } from '@astryxdesign/core/Section';
16
+ import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
17
+ import { StackItem } from '@astryxdesign/core/Stack';
18
+ import { pixel, proportional, Table } from '@astryxdesign/core/Table';
19
+ import { Text } from '@astryxdesign/core/Text';
20
+ import { TextArea } from '@astryxdesign/core/TextArea';
21
+ import { TextInput } from '@astryxdesign/core/TextInput';
22
+ import { Thumbnail } from '@astryxdesign/core/Thumbnail';
23
+ import { Token } from '@astryxdesign/core/Token';
24
+ import { Tooltip } from '@astryxdesign/core/Tooltip';
25
+ import { VStack } from '@astryxdesign/core/VStack';
26
+ import { IconAbc, IconAdjustmentsHorizontal, IconAlertTriangle, IconAlignLeft, IconAntennaBars1, IconAntennaBars2, IconAntennaBars3, IconAntennaBars4, IconAntennaBars5, IconAntennaBarsOff, IconArrowDown, IconArrowLeft, IconArrowUp, IconBan, IconBandage, IconBiohazard, IconBolt, IconBraces, IconBulb, IconBulbOff, IconCertificate, IconCheck, IconChevronRight, IconCircleDashed, IconDatabase, IconDeviceDesktop, IconEraser, IconEye, IconEyeOff, IconFeather, IconFlag, IconFlame, IconFlask, IconGauge, IconHandOff, IconHandStop, IconHeadset, IconHourglass, IconHttpDelete, IconHttpGet, IconHttpPatch, IconHttpPost, IconHttpPut, IconInfoCircle, IconInputAi, IconKey, IconLayersUnion, IconLockOpen, IconMessage, IconMicrophone, IconPackage, IconPaperclip, IconPencil, IconPlayerPause, IconPlayerPlay, IconPlayerStop, IconPlayerTrackNext, IconPlugConnected, IconPlus, IconRefresh, IconRegex, IconSearch, IconSend, IconServer, IconShieldLock, IconShieldOff, IconSquareRoundedNumber0, IconSquareRoundedNumber2, IconTool, IconTrash, IconUser, IconUserCheck, IconWaveSine, IconWriting, IconX, } from '@tabler/icons-react';
27
+ import { ATTACHMENT_ACCEPT_MIMES, CONTINUE_STOP_KINDS, fieldMeta, GOOGLE_BUILTIN_TOOLS, GOOGLE_IMAGE_ASPECT_RATIOS, GOOGLE_IMAGE_OUTPUT_MIMES, GOOGLE_IMAGE_RESOLUTIONS, GOOGLE_SPEECH_VOICES, HTTP_METHODS, IMAGE_ATTACHMENT_ACCEPT_MIMES, LABEL_KEYS, labelDefault, PROFILE_HANDLE_MAX_CHARS, PROFILE_ID_MAX_CHARS, profileGraphFacet, THINKING_LEVELS, VOICE_ACCEPT_MIMES, } from '@theoremjs/agents';
28
+ import { BOUNDARIES, BOUNDARY_META, DETECT_ACTION_META, DETECT_ACTIONS, DETECTOR_BOUNDARIES, DETECTOR_GROUP_META, DETECTOR_GROUPS, DETECTOR_META, DETECTORS, TOOL_BOUNDARIES, TOOL_KINDS, } from '@theoremjs/agents/guardrails';
29
+ import { lazy, Suspense, useContext, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from 'react';
30
+ import { acceptSections, agentToolTarget, bindingSlots, COMPACTION_DRAFT_DEFAULTS, defaultEffortRequired, defaultModelRequired, definitionOf, draftAllows, draftKey, expandAccept, INLINE_LABELS, includableFacets, includeFacet, inputLimitsRequired, isGoogleRoute, isOpenRouterRoute, kernelDefinitions, LOCAL_PROVIDER_ID, lentCompression, modelBindingNodeId, modelRoute, modelSlots, newCriteria, newDecisionImage, newDecisionQuestion, newModelBinding, newOwnDetector, newPattern, newToolSpec, nextAccept, onStudioLocal, optionAt, plantsCanary, protocolOptionFields, removeModelBinding, STUDIO_TRACE_DESTINATION, sampleToolInput, sectionNote, servedProtocols, servesProfileType, setProfileType, studioNetworkNote, studioNodeRef, studioRunsProvider, studioTaintNote, takesContinueInstruction, toolSpecNodeId, updateModelBinding, withOption, writeAsk, } from "../mod.js";
31
+ import { AddProviderButton } from "./add-provider.js";
32
+ import { IconGoogle, IconOpenAi, IconOpenRouter, IconTypeSafe } from "./brand-icons.js";
33
+ import { ChoiceRow, InspectorGroupTitle, InspectorRow, InspectorSection, ListRow, NamesRow, NumberRow, OriginLine, SegmentedRow, SetInCode, SliderRow, SwitchRow, TextAreaRow, TextRow, } from "./inspector.js";
34
+ import { ConfirmWrite, LeavePage, ListBadges, LocalConnection, NodeIssues, ProjectAsks, QuestionLink, RowOrigins, RunsOn, SaidOrigin, useFieldStatus, useRowOrigin, WorkspaceContext, } from "./inspector-context.js";
35
+ import { CONTEXT_PLACEHOLDER, contextErrorOf, draftSlots, usePageValues, } from "./lib/studio-page.js";
36
+ import { useProject } from "./lib/studio-project.js";
37
+ import { setToolCredential, subscribeToolCredentials, toolCredential, } from "./lib/tool-credentials.js";
38
+ import { runToolProbe } from "./lib/tool-probe.js";
39
+ import { PROFILE_TYPE_ICON, QUESTION_TYPE_ICON, TOOL_TYPE_ICON, toolTypeIcon, } from "./lib/type-icons.js";
40
+ import { slotDescription, useProviderModels } from "./studio-connection.js";
41
+ import { IconTheorem } from "./theorem-mark.js";
42
+ // The tester and its probes load when a detector's page first shows it.
43
+ const DetectorTester = lazy(() => import("./guardrail-tester.js").then((tester) => ({ default: tester.DetectorTester })));
44
+ /** Sets fields of one section of the draft, leaving the rest as it is. */
45
+ function patch(setDraft, key) {
46
+ return (change) => {
47
+ setDraft((draft) => ({ ...draft, [key]: { ...draft[key], ...change } }));
48
+ };
49
+ }
50
+ const PROFILE_TYPE_SEGMENTS = [
51
+ { value: 'text', label: 'Text', icon: PROFILE_TYPE_ICON.text },
52
+ { value: 'image', label: 'Image', icon: PROFILE_TYPE_ICON.image },
53
+ { value: 'speech', label: 'Speech', icon: PROFILE_TYPE_ICON.speech },
54
+ { value: 'live', label: 'Live', icon: PROFILE_TYPE_ICON.live },
55
+ { value: 'decision', label: 'Decision', icon: PROFILE_TYPE_ICON.decision },
56
+ { value: 'embedding', label: 'Embedding', icon: PROFILE_TYPE_ICON.embedding },
57
+ { value: 'host', label: 'Host', icon: PROFILE_TYPE_ICON.host },
58
+ ];
59
+ const HISTORY_HOLDER_SEGMENTS = [
60
+ { value: 'host', label: 'Host', icon: IconDeviceDesktop },
61
+ { value: 'stateless', label: 'Stateless', icon: IconBan },
62
+ { value: 'provider', label: 'Provider', icon: IconServer },
63
+ ];
64
+ /** How the studio's own providers are named. A project's provider is named by its id. */
65
+ const PROVIDER_LABEL = {
66
+ gemini: 'Gemini',
67
+ openrouter: 'OpenRouter',
68
+ [LOCAL_PROVIDER_ID]: 'Local',
69
+ typesafe: 'TypeSafe',
70
+ };
71
+ /** The mark of the service a kernel definition reaches. */
72
+ const DEFINITION_ICON = {
73
+ gemini: IconGoogle,
74
+ openrouter: IconOpenRouter,
75
+ local: IconOpenAi,
76
+ typesafe: IconTypeSafe,
77
+ };
78
+ const LEVEL_SEGMENT = {
79
+ none: { label: 'None', icon: IconAntennaBarsOff },
80
+ minimal: { label: 'Minimal', icon: IconAntennaBars1 },
81
+ low: { label: 'Low', icon: IconAntennaBars2 },
82
+ medium: { label: 'Medium', icon: IconAntennaBars3 },
83
+ high: { label: 'High', icon: IconAntennaBars4 },
84
+ xhigh: { label: 'Extra high', icon: IconAntennaBars5 },
85
+ max: { label: 'Max', icon: IconFlame },
86
+ };
87
+ const LEVEL_SEGMENTS = THINKING_LEVELS.map((value) => ({
88
+ value,
89
+ ...LEVEL_SEGMENT[value],
90
+ }));
91
+ /** Thought summaries: on, off, or left to the provider (`null` in the draft). */
92
+ const SUMMARIES_SEGMENTS = [
93
+ { value: 'default', label: 'Provider default', icon: IconCircleDashed },
94
+ { value: 'on', label: 'On', icon: IconBulb },
95
+ { value: 'off', label: 'Off', icon: IconBulbOff },
96
+ ];
97
+ /** A setting left to the provider (`null` in the draft), on, or off. */
98
+ const PROVIDER_DEFAULT_SEGMENT = { default: null, on: true, off: false };
99
+ function providerDefaultSegment(value) {
100
+ if (value === null)
101
+ return 'default';
102
+ return value ? 'on' : 'off';
103
+ }
104
+ const KIND_TITLE = {
105
+ image: 'Image',
106
+ video: 'Video',
107
+ document: 'Document',
108
+ audio: 'Audio',
109
+ };
110
+ /** A MIME allowlist's picker: a section per media kind, its wildcard first. */
111
+ function acceptPicker(mimes) {
112
+ const sections = acceptSections(mimes);
113
+ const options = sections.map(({ kind, wildcard, mimes: kindMimes }) => ({
114
+ type: 'section',
115
+ title: KIND_TITLE[kind],
116
+ options: [
117
+ ...(wildcard ? [{ value: wildcard, description: `Every ${kind} type` }] : []),
118
+ ...kindMimes.map((value) => ({ value })),
119
+ ],
120
+ }));
121
+ return { sections, options };
122
+ }
123
+ const ATTACHMENT_PICKER = acceptPicker(ATTACHMENT_ACCEPT_MIMES);
124
+ const IMAGE_ATTACHMENT_PICKER = acceptPicker(IMAGE_ATTACHMENT_ACCEPT_MIMES);
125
+ const VOICE_PICKER = acceptPicker(VOICE_ACCEPT_MIMES);
126
+ /** The page's lent keys, while the studio runs on them. */
127
+ function useLentKeys() {
128
+ const on = useContext(RunsOn);
129
+ return on.mode === 'demo' ? on.keys : undefined;
130
+ }
131
+ /**
132
+ * Moves a binding to another provider. It keeps its protocol where the new provider serves it, and
133
+ * that protocol's options with it; otherwise it takes the first protocol served and starts with no
134
+ * options. On lent keys it takes their first model there and keeps the built-ins that model may
135
+ * use; otherwise the builder picks the model, and only Google keeps them.
136
+ */
137
+ function reprovide(binding, type, provider, keys) {
138
+ const served = servedProtocols(provider, type);
139
+ const kept = !served || served.some((protocol) => protocol.id === binding.protocol);
140
+ const protocol = kept ? binding.protocol : (served[0]?.id ?? binding.protocol);
141
+ const apiId = keys?.models(type, provider.definition)[0]?.id ?? '';
142
+ const allowed = new Set(keys
143
+ ? keys.builtins(apiId)
144
+ : isGoogleRoute({ protocol })
145
+ ? GOOGLE_BUILTIN_TOOLS.map((tool) => tool.name)
146
+ : []);
147
+ return {
148
+ provider: provider.id,
149
+ protocol,
150
+ options: kept ? binding.options : {},
151
+ apiId,
152
+ builtInTools: binding.builtInTools.filter((id) => allowed.has(id)),
153
+ };
154
+ }
155
+ /**
156
+ * A type change turns on each optional section the new type brings that the old one didn't: the
157
+ * first pick shows them all. One the author took out under the old type stays out.
158
+ */
159
+ function withNewSections(before, after) {
160
+ const leftOut = new Set(includableFacets(before));
161
+ return includableFacets(after)
162
+ .filter((facet) => !leftOut.has(facet))
163
+ .reduce(includeFacet, after);
164
+ }
165
+ function SystemPromptSection({ identity, set, }) {
166
+ return (_jsxs(InspectorSection, { title: "System prompt", children: [_jsx(TextArea, { label: "System prompt", isLabelHidden: true, size: "sm", rows: 8, value: identity.system, placeholder: fieldMeta('identity.system')?.unset, onChange: (system) => {
167
+ set({ system });
168
+ } }), _jsx(TextAreaRow, { label: "By role", path: "identity.systemByRole", field: "systemByRoleJson", value: identity.systemByRoleJson, rows: 4, hasSpellCheck: false, placeholder: SYSTEM_BY_ROLE_PLACEHOLDER, onChange: (systemByRoleJson) => {
169
+ set({ systemByRoleJson });
170
+ } })] }));
171
+ }
172
+ function IdentityEditor({ draft, setDraft }) {
173
+ const keys = useLentKeys();
174
+ const { identity } = draft;
175
+ const set = patch(setDraft, 'identity');
176
+ return (_jsxs(_Fragment, { children: [_jsxs(InspectorSection, { title: "Profile", children: [_jsx(TextRow, { label: "Id", path: "profile.id", field: "agentId", value: identity.agentId, placeholder: "my.agent", maxLength: PROFILE_ID_MAX_CHARS, onChange: (agentId) => {
177
+ set({ agentId });
178
+ } }), _jsx(SegmentedRow, { label: "Type", path: "profile.type", field: "profileType", value: identity.profileType, segments: PROFILE_TYPE_SEGMENTS, onChange: (type) => {
179
+ if (type)
180
+ setDraft((current) => withNewSections(current, setProfileType(current, type, keys)));
181
+ } }), identity.profileType !== 'host' && (_jsx(TextRow, { label: "Handle", path: "identity.handle", field: "handle", value: identity.handle, placeholder: "agent", maxLength: PROFILE_HANDLE_MAX_CHARS, onChange: (handle) => {
182
+ set({ handle });
183
+ } }))] }), identity.profileType !== 'speech' &&
184
+ identity.profileType !== 'decision' &&
185
+ identity.profileType !== 'host' && _jsx(SystemPromptSection, { identity: identity, set: set })] }));
186
+ }
187
+ /** The policy every model of the profile shares: default, switching, steps, and key slots. */
188
+ function ModelPolicySection({ draft, type, set, }) {
189
+ const { models } = draft;
190
+ return (_jsxs(InspectorSection, { title: "Policy", children: [_jsx(ChoiceRow, { label: "Default", path: "profile.defaultModel", field: "defaultModel", value: models.defaultModel, isRequired: defaultModelRequired(draft), options: draft.modelBindings.map((binding) => binding.modelId), onChange: (defaultModel) => {
191
+ set({ defaultModel });
192
+ } }), _jsx(SwitchRow, { label: "Switching", path: "profile.allowModelSelect", field: "allowModelSelect", value: models.allowModelSelect, onChange: (allowModelSelect) => {
193
+ set({ allowModelSelect });
194
+ } }), type !== 'embedding' && (_jsxs(_Fragment, { children: [_jsx(SegmentedRow, { label: "Conversation history kept by", path: "providerContinuation.history", field: "providerHistory", value: models.providerHistory ?? 'host', segments: HISTORY_HOLDER_SEGMENTS, onChange: (providerHistory) => {
195
+ set({ providerHistory });
196
+ } }), _jsx(NumberRow, { label: "Max steps", path: "profile.maxSteps", units: "steps", field: "maxSteps", value: models.maxSteps, min: 1, isIntegerOnly: true, onChange: (maxSteps) => {
197
+ set({ maxSteps });
198
+ } })] })), _jsx(SlotRow, { label: "Key slot", path: "models.*.keySlot", field: "key", value: models.key, onChange: (key) => {
199
+ set({ key });
200
+ } }), _jsx(SlotRow, { label: "Fallback slot", path: "models.*.fallbackKeySlot", field: "fallbackKey", value: models.fallbackKey ?? '', onChange: (fallbackKey) => {
201
+ set({ fallbackKey });
202
+ } })] }));
203
+ }
204
+ function ModelsEditor({ draft, setDraft, onSelect, }) {
205
+ const set = patch(setDraft, 'models');
206
+ const start = useLentKeys()?.start;
207
+ const addModel = () => {
208
+ const binding = newModelBinding(draft, start);
209
+ setDraft((current) => ({ ...current, modelBindings: [...current.modelBindings, binding] }));
210
+ onSelect(modelBindingNodeId(binding.key));
211
+ };
212
+ return (_jsxs(_Fragment, { children: [_jsx(InspectorSection, { note: draft.identity.profileType === 'decision'
213
+ ? sectionNote('models.decision')
214
+ : sectionNote('models'), children: _jsx(Button, { label: "Add model", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), isDisabled: draft.identity.profileType === 'decision' && draft.modelBindings.length >= 1, onClick: addModel }) }), draft.identity.profileType !== 'decision' && (_jsx(ModelPolicySection, { draft: draft, type: bindingProfileType(draft.identity.profileType), set: set }))] }));
215
+ }
216
+ /** A decision binding's wire model: picked from the demo's list, typed outside the demo. */
217
+ function DecisionApiModelRow({ binding, definition, set, }) {
218
+ const keys = useLentKeys();
219
+ if (!keys)
220
+ return (_jsx(TextRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: binding.apiId, isRequired: true, onChange: (apiId) => {
221
+ set({ apiId });
222
+ } }));
223
+ return (_jsx(ChoiceRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: binding.apiId, options: keys.models('decision', definition).map((model) => ({
224
+ value: model.id,
225
+ label: model.label,
226
+ description: model.id,
227
+ })), onChange: (apiId) => {
228
+ set({ apiId });
229
+ } }));
230
+ }
231
+ /** Decision model binding, using the same provider and API id rows as other models. */
232
+ function DecisionModelEditor({ draft, setDraft, bindingKey, }) {
233
+ const keys = useLentKeys();
234
+ const binding = draft.modelBindings.find((candidate) => candidate.key === bindingKey);
235
+ if (!binding)
236
+ return null;
237
+ const set = (change) => {
238
+ setDraft((current) => updateModelBinding(current, bindingKey, change));
239
+ };
240
+ return (_jsxs(InspectorSection, { title: "Decision model", note: sectionNote('decisionModel'), children: [_jsx(TextRow, { label: "Id", path: "models.*", field: "modelId", value: binding.modelId, isRequired: true, onChange: (modelId) => {
241
+ set({ modelId });
242
+ } }), _jsx(ProviderRow, { providers: draft.providers, binding: binding, type: "decision", set: set }), _jsx(ProtocolRow, { providers: draft.providers, binding: binding, type: "decision", set: set }), _jsx(DecisionApiModelRow, { binding: binding, definition: definitionOf(draft.providers, binding.provider), set: set }), _jsx(ProtocolOptionRows, { providers: draft.providers, binding: binding, type: "decision", set: set }), _jsx(SlotRow, { label: "Key slot", path: "models.*.keySlot", value: draft.models.key, onChange: (key) => {
243
+ setDraft((current) => ({ ...current, models: { ...current.models, key } }));
244
+ } }), _jsx(NumberRow, { label: "Timeout", path: "models.*.timeoutMs", units: "ms", field: "timeoutMs", value: binding.timeoutMs, min: 1, max: keys?.decision.timeoutMs, isIntegerOnly: true, onChange: (timeoutMs) => {
245
+ set({ timeoutMs });
246
+ } })] }));
247
+ }
248
+ /** A key slot, chosen from the slots the keys panel holds; each says which service its key is for. */
249
+ export function SlotRow({ label, path, field, value, isRequired, given, onChange, }) {
250
+ const connection = useContext(LocalConnection);
251
+ const hint = given ? `${given}, from its provider` : undefined;
252
+ if (!connection)
253
+ return (_jsx(TextRow, { label: label, path: path, field: field, value: value, isRequired: isRequired, hint: hint, onChange: onChange }));
254
+ // A slot the profile names but the panel doesn't hold yet stays choosable.
255
+ const slots = value && !connection.slots.includes(value) ? [...connection.slots, value] : connection.slots;
256
+ return (_jsx(ChoiceRow, { label: label, path: path, field: field, value: value, isRequired: isRequired, options: slots.map((slot) => ({
257
+ value: slot,
258
+ label: slot,
259
+ description: slotDescription(connection.vault[slot]),
260
+ })), emptyText: "Add a key under Keys first.", placeholder: hint, onChange: onChange }));
261
+ }
262
+ /**
263
+ * The provider's model, picked from what it lists for this profile type with the key in the
264
+ * model's slot. Until there is a list (a Gemini model with no key, or a list that failed), the id
265
+ * is typed.
266
+ */
267
+ function ProviderModelRow({ definition, type, slot, value, onChange, }) {
268
+ const connection = useContext(LocalConnection);
269
+ const listed = definition === 'gemini' || definition === 'openrouter' ? definition : undefined;
270
+ const models = useProviderModels(listed, type, slot ? connection?.vault[slot] : undefined);
271
+ if (models.status === 'idle' || models.status === 'error')
272
+ return (_jsx(TextRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: value, isRequired: true, placeholder: listed === 'gemini' ? 'Choose a key to list models' : 'Provider model id', status: models.status === 'error' ? { type: 'error', message: models.error } : undefined, onChange: onChange }));
273
+ const options = models.models.map((model) => ({
274
+ value: model.id,
275
+ label: model.label,
276
+ description: model.id,
277
+ }));
278
+ return (_jsx(ChoiceRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: value, options: options, isLoading: models.status === 'loading', emptyText: `No ${type} models listed.`, hasSearch: true, isRequired: true, onChange: onChange }));
279
+ }
280
+ /** The profile type a model runs as: a decision has its own binding editor; a host has no model. */
281
+ function bindingProfileType(chosen) {
282
+ return chosen && chosen !== 'decision' && chosen !== 'host' ? chosen : 'text';
283
+ }
284
+ /** A provider's segment, disabled with the reason when the studio can't run it here. */
285
+ function providerSegment(provider, type, on, inProject) {
286
+ const local = onStudioLocal(provider.id, inProject);
287
+ return {
288
+ value: provider.id,
289
+ label: PROVIDER_LABEL[provider.id] ?? provider.id,
290
+ icon: local ? IconDeviceDesktop : (DEFINITION_ICON[provider.definition] ?? IconServer),
291
+ isDisabled: !studioRunsProvider(type, provider, local ? { mode: 'local' } : on),
292
+ disabledMessage: on.mode === 'demo' && !local
293
+ ? 'Add your own key under Keys to use it.'
294
+ : `It doesn't run ${type} agents.`,
295
+ };
296
+ }
297
+ /** Past this many providers, the row is a dropdown: more icons are too small to tell apart. */
298
+ const MOST_PROVIDER_SEGMENTS = 4;
299
+ /** The workspace's providers that serve this kind of agent, each enabled only where the studio runs it. */
300
+ function ProviderRow({ providers, binding, type, set, }) {
301
+ const on = useContext(RunsOn);
302
+ const keys = on.mode === 'demo' ? on.keys : undefined;
303
+ const inProject = useProject() !== null;
304
+ const add = useContext(WorkspaceContext)?.addProvider;
305
+ const serving = providers.filter((provider) => provider.id === binding.provider || servesProfileType(provider, type));
306
+ // A new provider starts on the definition of the one the model is on, so the model keeps its API id.
307
+ const now = providers.find((provider) => provider.id === binding.provider)?.definition;
308
+ const serves = kernelDefinitions().filter((definition) => servesProfileType({ id: definition, definition }, type));
309
+ const definitions = now && serves.includes(now)
310
+ ? [now, ...serves.filter((definition) => definition !== now)]
311
+ : serves;
312
+ return (_jsx(SegmentedRow, { label: "Provider", path: "models.*.provider", field: "provider", value: binding.provider, segments: serving.map((provider) => providerSegment(provider, type, on, inProject)), selectorPast: MOST_PROVIDER_SEGMENTS, onChange: (id) => {
313
+ const provider = providers.find((each) => each.id === id);
314
+ if (provider)
315
+ set(reprovide(binding, type, provider, keys));
316
+ }, trailing: add && (_jsx(AddProviderButton, { isCompact: true, taken: providers.map((provider) => provider.id), definitions: definitions,
317
+ // The model moves to it, and its editor opens for what it connects with.
318
+ onAdd: (provider) => {
319
+ set(reprovide(binding, type, provider, keys));
320
+ add(provider);
321
+ } })) }));
322
+ }
323
+ /**
324
+ * A local model's endpoint, with help when the page's origin can't reach it. It is the tab's
325
+ * connection, so changing it leaves the profile's model as it is: a model the new endpoint does
326
+ * not list shows as not in the list.
327
+ */
328
+ function LocalEndpointRows() {
329
+ const localConnection = useContext(LocalConnection);
330
+ if (!localConnection)
331
+ return null;
332
+ const failed = localConnection.localModels.status === 'error';
333
+ return (_jsxs(_Fragment, { children: [_jsx(TextRow, { label: "Endpoint", path: "local.baseUrl", value: localConnection.local.baseUrl, placeholder: "http://127.0.0.1:11434", status: failed ? { type: 'error', message: localConnection.localModels.error ?? '' } : undefined, onChange: (baseUrl) => {
334
+ localConnection.setLocal((current) => ({ ...current, baseUrl }));
335
+ } }), failed && !isLocalPage() && _jsx(LocalOriginHelp, {})] }));
336
+ }
337
+ /** A hosted model's key slot and fallback slot; left blank, each is the one its provider gives. */
338
+ function KeySlotRows({ providers, binding, set, }) {
339
+ const given = modelSlots(providers, { provider: binding.provider });
340
+ return (_jsxs(_Fragment, { children: [_jsx(SlotRow, { label: "Key slot", path: "models.*.keySlot", field: "keySlot", given: given.keySlot, value: binding.keySlot ?? '', onChange: (keySlot) => {
341
+ set({ keySlot });
342
+ } }), _jsx(SlotRow, { label: "Fallback slot", path: "models.*.fallbackKeySlot", field: "fallbackKeySlot", given: given.fallbackKeySlot, value: binding.fallbackKeySlot ?? '', onChange: (fallbackKeySlot) => {
343
+ set({ fallbackKeySlot });
344
+ } })] }));
345
+ }
346
+ /** A local model, picked from what the endpoint lists, or typed while the endpoint lists nothing. */
347
+ function LocalApiModelRow({ binding, localConnection, set, }) {
348
+ const { status } = localConnection.localModels;
349
+ const onChange = (apiId) => {
350
+ set({ apiId });
351
+ };
352
+ if (status === 'idle' || status === 'error')
353
+ return (_jsx(TextRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: binding.apiId, isRequired: true, placeholder: "Local model id", status: status === 'error'
354
+ ? {
355
+ type: 'error',
356
+ message: "Can't reach the endpoint. Start it to list its models, or type the id.",
357
+ }
358
+ : undefined, onChange: onChange }));
359
+ return (_jsx(ChoiceRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: binding.apiId, options: localConnection.localModels.ids, isLoading: status === 'loading', emptyText: "This server has no models.", hasSearch: true, isRequired: true, onChange: onChange }));
360
+ }
361
+ /** A model the lent keys offer here; built-ins the model may not use on them are dropped. */
362
+ function LentApiModelRow({ binding, type, definition, keys, set, }) {
363
+ return (_jsx(ChoiceRow, { label: "API model", path: "models.*.apiId", field: "apiId", value: binding.apiId, options: keys.models(type, definition).map((model) => ({
364
+ value: model.id,
365
+ label: model.label,
366
+ description: model.id,
367
+ })), onChange: (apiId) => {
368
+ const allowed = new Set(keys.builtins(apiId));
369
+ set({ apiId, builtInTools: binding.builtInTools.filter((id) => allowed.has(id)) });
370
+ } }));
371
+ }
372
+ /** The model's API id: from the local endpoint, the lent keys' list, or the provider's list. */
373
+ function ApiModelRow({ draft, binding, type, set, }) {
374
+ const keys = useLentKeys();
375
+ const localConnection = useContext(LocalConnection);
376
+ const inProject = useProject() !== null;
377
+ const { definition } = modelRoute(draft.providers, type, binding);
378
+ if (onStudioLocal(binding.provider, inProject) && localConnection)
379
+ return _jsx(LocalApiModelRow, { binding: binding, localConnection: localConnection, set: set });
380
+ if (keys)
381
+ return (_jsx(LentApiModelRow, { binding: binding, type: type, definition: definition, keys: keys, set: set }));
382
+ return (_jsx(ProviderModelRow, { definition: definition, type: type, slot: bindingSlots(draft, binding).keySlot ?? '', value: binding.apiId, onChange: (apiId) => {
383
+ set({ apiId });
384
+ } }));
385
+ }
386
+ /** Whether a local model may call the project's remote tools. */
387
+ function RemoteToolsRow() {
388
+ const localConnection = useContext(LocalConnection);
389
+ if (!localConnection)
390
+ return null;
391
+ return (_jsx(SwitchRow, { label: "Remote tools", path: "studio.remoteTools", value: localConnection.remoteTools, onChange: localConnection.setRemoteTools }));
392
+ }
393
+ /**
394
+ * The protocol the model is spoken to in, picked from those its provider serves for this kind of
395
+ * agent. Shown when there is a choice, or when the model names one the provider doesn't serve; a
396
+ * provider from a project's own definition takes the protocol typed.
397
+ */
398
+ function ProtocolRow({ providers, binding, type, set, }) {
399
+ const provider = providers.find((each) => each.id === binding.provider);
400
+ const served = provider && servedProtocols(provider, type);
401
+ if (!served)
402
+ return (_jsx(TextRow, { label: "Protocol", path: "models.*.protocol", field: "protocol", value: binding.protocol, isRequired: true, onChange: (protocol) => {
403
+ set({ protocol, options: {} });
404
+ } }));
405
+ if (served.length === 1 && served[0]?.id === binding.protocol)
406
+ return null;
407
+ return (_jsx(ChoiceRow, { label: "Protocol", path: "models.*.protocol", field: "protocol", value: binding.protocol, options: served.map((protocol) => ({
408
+ value: protocol.id,
409
+ label: protocol.name,
410
+ description: protocol.description,
411
+ })), isRequired: true, onChange: (protocol) => {
412
+ if (protocol !== binding.protocol)
413
+ set({ protocol, options: {} });
414
+ } }));
415
+ }
416
+ /** The protocol's own options, a row per setting its schema describes; a provider of the project's own leaves them to code. */
417
+ function ProtocolOptionRows({ providers, binding, type, set, }) {
418
+ const protocol = protocolOf(providers, binding, type);
419
+ if (!protocol)
420
+ return null;
421
+ return (_jsx(_Fragment, { children: protocolOptionFields(protocol, binding).map((option) => (_jsx(ProtocolOptionRow, { option: option, binding: binding, set: set }, option.path))) }));
422
+ }
423
+ /** The protocol `binding` is spoken to in, where its provider is one of the kernel's own and serves it. */
424
+ function protocolOf(providers, binding, type) {
425
+ const provider = providers.find((each) => each.id === binding.provider);
426
+ return provider && servedProtocols(provider, type)?.find((each) => each.id === binding.protocol);
427
+ }
428
+ /** A model's protocol options, as a section of their own, named for the protocol. */
429
+ function ProtocolOptionsSection({ providers, binding, type, set, }) {
430
+ const protocol = protocolOf(providers, binding, type);
431
+ if (!protocol || protocolOptionFields(protocol, binding).length === 0)
432
+ return null;
433
+ return (_jsx(InspectorSection, { title: protocol.name, path: "models.*.options", children: _jsx(ProtocolOptionRows, { providers: providers, binding: binding, type: type, set: set }) }));
434
+ }
435
+ /** A setting a boolean option takes: left to the protocol, on, or off. */
436
+ const OPTION_SWITCH_SEGMENTS = [
437
+ { value: 'default', label: 'Protocol default', icon: IconCircleDashed },
438
+ { value: 'on', label: 'On', icon: IconCheck },
439
+ { value: 'off', label: 'Off', icon: IconX },
440
+ ];
441
+ /** One protocol option, edited by the row its kind takes. */
442
+ function ProtocolOptionRow({ option, binding, set, }) {
443
+ const value = optionAt(binding.options, option.path);
444
+ const field = `options.${option.path}`;
445
+ const write = (next) => {
446
+ set({ options: withOption(binding.options, option.path, next) });
447
+ };
448
+ const common = {
449
+ label: option.label,
450
+ path: 'models.*.options',
451
+ field,
452
+ isRequired: option.required,
453
+ };
454
+ switch (option.kind) {
455
+ case 'boolean':
456
+ return (_jsx(SegmentedRow, { ...common, value: value === undefined ? 'default' : value ? 'on' : 'off', segments: OPTION_SWITCH_SEGMENTS, onChange: (segment) => {
457
+ write(segment === 'default' ? undefined : segment === 'on');
458
+ } }));
459
+ case 'choice':
460
+ return (_jsx(ChoiceRow, { ...common, value: option.choiceType === 'number' && typeof value === 'number'
461
+ ? String(value)
462
+ : typeof value === 'string'
463
+ ? value
464
+ : '', options: option.choices ?? [], hasHoverDescriptions: true, onChange: (next) => {
465
+ write(next ? (option.choiceType === 'number' ? Number(next) : next) : undefined);
466
+ } }));
467
+ case 'number':
468
+ return (_jsx(NumberRow, { ...common, value: typeof value === 'number' ? value : null, hint: option.description, onChange: (next) => {
469
+ write(next ?? undefined);
470
+ } }));
471
+ case 'text':
472
+ return (_jsx(TextRow, { ...common, value: typeof value === 'string' ? value : '', hint: option.description, onChange: (next) => {
473
+ write(next || undefined);
474
+ } }));
475
+ }
476
+ }
477
+ /** A Google model's built-ins: on lent keys, those its model may use on them. */
478
+ function BuiltinsRow({ binding, set }) {
479
+ const keys = useLentKeys();
480
+ const builtins = keys
481
+ ? [...keys.builtins(binding.apiId)]
482
+ : GOOGLE_BUILTIN_TOOLS.map((tool) => tool.name);
483
+ return (_jsx(ListRow, { label: "Built-ins", path: "models.*.builtInTools", field: "builtInTools", value: binding.builtInTools, options: builtins, onChange: (builtInTools) => {
484
+ set({ builtInTools });
485
+ } }));
486
+ }
487
+ /** Which model the binding runs: its id, transport, key slots, and API model. */
488
+ function ModelSection({ draft, binding, type, set, }) {
489
+ const google = isGoogleRoute(modelRoute(draft.providers, type, binding));
490
+ const local = onStudioLocal(binding.provider, useProject() !== null);
491
+ return (_jsxs(InspectorSection, { path: "models.*", children: [_jsx(TextRow, { label: "Id", path: "models.*", field: "modelId",
492
+ // A model\'s id is its key in the profile\'s models, so it can\'t be left out.
493
+ isRequired: true, value: binding.modelId, placeholder: "fast", onChange: (modelId) => {
494
+ set({ modelId });
495
+ } }), _jsx(ProviderRow, { providers: draft.providers, binding: binding, type: type, set: set }), _jsx(ProtocolRow, { providers: draft.providers, binding: binding, type: type, set: set }), local ? (_jsx(LocalEndpointRows, {})) : (_jsx(KeySlotRows, { providers: draft.providers, binding: binding, set: set })), _jsx(ApiModelRow, { draft: draft, binding: binding, type: type, set: set }), local && type !== 'embedding' && _jsx(RemoteToolsRow, {}), google && type !== 'embedding' && _jsx(BuiltinsRow, { binding: binding, set: set })] }));
496
+ }
497
+ /** How the model generates: its output cap, temperature, and summaries. */
498
+ function GenerationSection({ binding, set }) {
499
+ return (_jsxs(InspectorSection, { title: "Generation", children: [_jsx(NumberRow, { label: "Max output", path: "models.*.maxOutputTokens", units: "tokens", field: "maxOutputTokens", value: binding.maxOutputTokens, min: 1, isIntegerOnly: true, onChange: (maxOutputTokens) => {
500
+ set({ maxOutputTokens });
501
+ } }), _jsx(NumberRow, { label: "Temperature", path: "models.*.temperature", field: "temperature", value: binding.temperature, min: 0, step: 0.1, onChange: (temperature) => {
502
+ set({ temperature });
503
+ } }), _jsx(SegmentedRow, { label: "Summaries", path: "models.*.summaries", value: providerDefaultSegment(binding.summaries), segments: SUMMARIES_SEGMENTS, onChange: (segment) => {
504
+ set({ summaries: PROVIDER_DEFAULT_SEGMENT[segment] });
505
+ } })] }));
506
+ }
507
+ /** One effort: its alias, a remove button, and its level. */
508
+ function EffortRow({ efforts, index, setEfforts, }) {
509
+ const statusAt = useFieldStatus();
510
+ const status = statusAt('efforts', index);
511
+ const effort = efforts[index];
512
+ return (_jsxs(VStack, { gap: 3, children: [_jsxs(InspectorRow, { label: `Effort ${String(index + 1)}`, path: "models.*.efforts", hasIssue: status !== undefined, children: [_jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: `Effort ${String(index + 1)} alias`, isLabelHidden: true, size: "sm", status: status, value: effort.alias, placeholder: "Alias", onChange: (alias) => {
513
+ setEfforts(efforts.map((e, i) => (i === index ? { ...e, alias } : e)), { from: effort.alias, to: alias });
514
+ } }) }), _jsx(IconButton, { label: `Remove effort ${String(index + 1)}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: () => {
515
+ setEfforts(efforts.filter((_, i) => i !== index));
516
+ } })] }), _jsx(SegmentedRow, { label: "Level", path: "models.*.efforts.*", value: effort.level, segments: LEVEL_SEGMENTS, onChange: (level) => {
517
+ setEfforts(efforts.map((e, i) => (i === index ? { ...e, level } : e)));
518
+ } })] }));
519
+ }
520
+ /** The model's named efforts, its default effort, and whether people can switch. */
521
+ function EffortsSection({ binding, set }) {
522
+ const aliases = binding.efforts.map((effort) => effort.alias).filter(Boolean);
523
+ /**
524
+ * Sets the efforts and keeps the settings that depend on them valid: the default follows its
525
+ * alias through a rename (`renamed`) and is cleared when the alias goes, and effort switching
526
+ * turns off below two aliases, where its switch is disabled and couldn't be turned off.
527
+ */
528
+ const setEfforts = (efforts, renamed) => {
529
+ const next = new Set(efforts.map((effort) => effort.alias).filter(Boolean));
530
+ const defaultEffort = renamed && binding.defaultEffort === renamed.from ? renamed.to : binding.defaultEffort;
531
+ set({
532
+ efforts,
533
+ defaultEffort: next.has(defaultEffort) ? defaultEffort : '',
534
+ allowEffortSelect: binding.allowEffortSelect && next.size >= 2,
535
+ });
536
+ };
537
+ return (_jsxs(InspectorSection, { title: "Efforts", path: "models.*.efforts", children: [binding.efforts.map((_, index) => (_jsx(EffortRow, { efforts: binding.efforts, index: index, setEfforts: setEfforts }, index))), _jsx(Button, { label: "Add effort", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), onClick: () => {
538
+ setEfforts([...binding.efforts, { alias: '', level: 'medium' }]);
539
+ } }), _jsx(ChoiceRow, { label: "Default", path: "models.*.defaultEffort", field: "defaultEffort", value: binding.defaultEffort, isRequired: defaultEffortRequired(binding), options: aliases, onChange: (defaultEffort) => {
540
+ set({ defaultEffort });
541
+ } }), _jsx(SwitchRow, { label: "Switching", path: "models.*.allowEffortSelect", field: "allowEffortSelect", value: binding.allowEffortSelect, isDisabled: aliases.length < 2, disabledMessage: "Add a second effort to let people switch.", onChange: (allowEffortSelect) => {
542
+ set({ allowEffortSelect });
543
+ } })] }));
544
+ }
545
+ function ModelBindingEditor({ draft, setDraft, bindingKey, onSelect, }) {
546
+ const binding = draft.modelBindings.find((candidate) => candidate.key === bindingKey);
547
+ if (!binding)
548
+ return null;
549
+ const type = bindingProfileType(draft.identity.profileType);
550
+ const set = (change) => {
551
+ setDraft((current) => updateModelBinding(current, bindingKey, change));
552
+ };
553
+ return (_jsxs(_Fragment, { children: [_jsx(ModelSection, { draft: draft, binding: binding, type: type, set: set }), type !== 'embedding' && _jsx(GenerationSection, { binding: binding, set: set }), _jsx(ProtocolOptionsSection, { providers: draft.providers, binding: binding, type: type, set: set }), type === 'text' && _jsx(CompactionSection, { binding: binding, set: set }), type !== 'embedding' && _jsx(EffortsSection, { binding: binding, set: set }), draft.modelBindings.length > 1 && (_jsx(Section, { variant: "transparent", padding: 3, children: _jsx(Button, { label: "Remove model", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconTrash, size: "sm" }), onClick: () => {
554
+ setDraft((current) => removeModelBinding(current, bindingKey));
555
+ onSelect('models');
556
+ } }) }))] }));
557
+ }
558
+ function InputAcceptsSection({ inputs, set, image }) {
559
+ const attachmentPicker = image ? IMAGE_ATTACHMENT_PICKER : ATTACHMENT_PICKER;
560
+ return (_jsxs(InspectorSection, { title: "Accepts", path: "inputs", children: [_jsx(SwitchRow, { label: "Text", path: "inputs.text", value: inputs.text, onChange: (text) => {
561
+ set({ text });
562
+ } }), _jsx(ListRow, { label: "Attachments", path: "inputs.attachments.accept", value: expandAccept(inputs.attachmentsAccept, attachmentPicker.sections), options: attachmentPicker.options, onChange: (selected) => {
563
+ set({
564
+ attachmentsAccept: nextAccept(inputs.attachmentsAccept, selected, attachmentPicker.sections),
565
+ });
566
+ } }), !image && (_jsx(ListRow, { label: "Voice", path: "inputs.voice.accept", value: expandAccept(inputs.voiceAccept, VOICE_PICKER.sections), options: VOICE_PICKER.options, onChange: (selected) => {
567
+ set({ voiceAccept: nextAccept(inputs.voiceAccept, selected, VOICE_PICKER.sections) });
568
+ } }))] }));
569
+ }
570
+ function InputLimitsSection({ inputs, set }) {
571
+ const limitsRequired = inputLimitsRequired(inputs);
572
+ return (_jsxs(InspectorSection, { title: "Limits", children: [_jsx(NumberRow, { label: "Max files", path: "inputs.maxFiles", units: "files", field: "maxFiles", isRequired: limitsRequired, value: inputs.maxFiles, min: 0, isIntegerOnly: true, onChange: (maxFiles) => {
573
+ set({ maxFiles });
574
+ } }), _jsx(NumberRow, { label: "Max bytes", path: "inputs.maxBytes", units: "bytes", field: "maxBytes", isRequired: limitsRequired, value: inputs.maxBytes, min: 0, isIntegerOnly: true, onChange: (maxBytes) => {
575
+ set({ maxBytes });
576
+ } }), _jsx(NumberRow, { label: "Turn bytes", path: "inputs.maxTurnBytes", units: "bytes", field: "maxTurnBytes", isRequired: limitsRequired, value: inputs.maxTurnBytes, min: 0, isIntegerOnly: true, onChange: (maxTurnBytes) => {
577
+ set({ maxTurnBytes });
578
+ } }), _jsx(TextAreaRow, { label: "By type", path: "inputs.limitsByMime", field: "limitsByMimeJson", value: inputs.limitsByMimeJson, rows: 3, hasSpellCheck: false, placeholder: LIMITS_BY_MIME_PLACEHOLDER, onChange: (limitsByMimeJson) => {
579
+ set({ limitsByMimeJson });
580
+ } })] }));
581
+ }
582
+ const CONTEXT_SENDER_LABELS = {
583
+ client: 'The page',
584
+ server: 'The server',
585
+ };
586
+ const CONTEXT_SENDERS = Object.keys(CONTEXT_SENDER_LABELS);
587
+ /** The size a context starts with when its first sender is ticked. */
588
+ const CONTEXT_MAX_CHARS_START = 4000;
589
+ /** Who may tell the agent what the page shows, and how much. */
590
+ function InputContextSection({ inputs, set }) {
591
+ return (_jsxs(InspectorSection, { title: "Context", path: "inputs.context", note: sectionNote('context'), children: [_jsx(InspectorRow, { label: "From", path: "inputs.context.from", children: _jsx(StackItem, { size: "fill", children: _jsx(CheckboxList, { label: "From", isLabelHidden: true, density: "compact", value: inputs.contextFrom, onChange: (checked) => {
592
+ set({
593
+ contextFrom: CONTEXT_SENDERS.filter((sender) => checked.includes(sender)),
594
+ // why: The kernel has no default size; the first sender starts with one to edit.
595
+ contextMaxChars: inputs.contextMaxChars ?? CONTEXT_MAX_CHARS_START,
596
+ });
597
+ }, children: CONTEXT_SENDERS.map((sender) => (_jsx(CheckboxListItem, { value: sender, label: CONTEXT_SENDER_LABELS[sender] }, sender))) }) }) }), inputs.contextFrom.length > 0 && (_jsx(NumberRow, { label: "Max size", path: "inputs.context.maxChars", units: "characters", field: "contextMaxChars", isRequired: true, value: inputs.contextMaxChars, min: 1, isIntegerOnly: true, onChange: (contextMaxChars) => {
598
+ set({ contextMaxChars });
599
+ } })), inputs.contextFrom.includes('client') && _jsx(ContextPreviewRow, {})] }));
600
+ }
601
+ /** The context the studio's page sends the open agent. Kept in this tab, not in the profile. */
602
+ function ContextPreviewRow() {
603
+ const [values, setValues] = usePageValues(useContext(WorkspaceContext)?.self ?? '');
604
+ const error = contextErrorOf(values.contextJson);
605
+ return (_jsx(TextAreaRow, { label: "Preview", path: "inputs.context", isRequired: false, value: values.contextJson, rows: 3, hasSpellCheck: false, placeholder: CONTEXT_PLACEHOLDER, status: error ? { type: 'error', message: error } : undefined, onChange: (contextJson) => {
606
+ setValues({ ...values, contextJson });
607
+ } }));
608
+ }
609
+ /** The value the studio's page sends for each slot declared so far. Kept in this tab. */
610
+ function SlotPreviewRows({ slotsJson }) {
611
+ const [values, setValues] = usePageValues(useContext(WorkspaceContext)?.self ?? '');
612
+ const slots = draftSlots(slotsJson);
613
+ return Object.entries(slots).map(([name, allowed]) => (_jsx(ChoiceRow, { label: name, path: "inputs.slots", isRequired: true, value: allowed.find((value) => value === values.slots[name]) ?? allowed[0], options: allowed, onChange: (next) => {
614
+ if (next)
615
+ setValues({ ...values, slots: { ...values.slots, [name]: next } });
616
+ } }, name)));
617
+ }
618
+ function InputsEditor({ draft, setDraft }) {
619
+ const { inputs } = draft;
620
+ const set = patch(setDraft, 'inputs');
621
+ /** An image profile takes images, video, and PDF as references, and no voice. */
622
+ const image = draft.identity.profileType === 'image';
623
+ return (_jsxs(_Fragment, { children: [draftAllows(draft, 'inputs.attachments.accept') && (_jsx(InputAcceptsSection, { inputs: inputs, set: set, image: image })), draftAllows(draft, 'inputs.maxFiles') && _jsx(InputLimitsSection, { inputs: inputs, set: set }), draftAllows(draft, 'inputs.context') && _jsx(InputContextSection, { inputs: inputs, set: set }), _jsxs(InspectorSection, { path: "inputs.slots", note: sectionNote('slots'), children: [_jsx(TextAreaRow, { label: "Slots", path: "inputs.slots", field: "slotsJson", value: inputs.slotsJson, rows: 3, hasSpellCheck: false, placeholder: SLOTS_PLACEHOLDER, onChange: (slotsJson) => {
624
+ set({ slotsJson });
625
+ } }), _jsx(SlotPreviewRows, { slotsJson: inputs.slotsJson })] })] }));
626
+ }
627
+ const SYSTEM_BY_ROLE_PLACEHOLDER = `{
628
+ "support": "Answer as the support desk."
629
+ }`;
630
+ const LIMITS_BY_MIME_PLACEHOLDER = `{
631
+ "application/pdf": 5000000
632
+ }`;
633
+ const SLOTS_PLACEHOLDER = `{
634
+ "language": ["en", "fr"]
635
+ }`;
636
+ /** The agents another agent can name: every one but itself, each by its id. */
637
+ function otherAgents(agents, self, canName) {
638
+ return agents
639
+ .filter((agent) => agent.key !== self)
640
+ .map((agent) => ({
641
+ value: agent.key,
642
+ label: agent.agentId || 'Unnamed agent',
643
+ disabled: !canName(agent),
644
+ }));
645
+ }
646
+ /** The other text agent that writes the summary, where the workspace has one to name. */
647
+ function CompactionSummariserRow({ binding, set, }) {
648
+ const workspace = useContext(WorkspaceContext);
649
+ // Kept while it names an agent, so a removed one can still be cleared here.
650
+ if (!workspace || !(workspace.agents.length > 1 || binding.compactWith))
651
+ return null;
652
+ return (_jsx(ChoiceRow, { label: "Summarised by", path: "models.*.compaction.profile", field: "compactWith", isRequired: false, value: binding.compactWith ?? '', options: otherAgents(workspace.agents, workspace.self, (agent) => agent.type === 'text'), onChange: (compactWith) => {
653
+ set({ compactWith });
654
+ } }));
655
+ }
656
+ /** How compaction runs once it has a timing: its budget, threshold, kept turns, and meter. */
657
+ function CompactionRows({ binding, set }) {
658
+ return (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Budget", path: "models.*.compaction.maxTokens", field: "compactMaxTokens", value: binding.compactMaxTokens ?? null, min: 1, isIntegerOnly: true, units: "tokens", onChange: (compactMaxTokens) => {
659
+ set({ compactMaxTokens });
660
+ } }), _jsx(SliderRow, { label: "Starts at", path: "models.*.compaction.compactAt", field: "compactAt", value: binding.compactAt ?? COMPACTION_DRAFT_DEFAULTS.compactAt, min: 0.05, max: 0.95, step: 0.05, format: (at) => PERCENT.format(at), onChange: (compactAt) => {
661
+ set({ compactAt });
662
+ } }), _jsx(NumberRow, { label: "Keep", path: "models.*.compaction.previousExchanges", field: "compactKeep", value: binding.compactKeep ?? null, min: 0, onChange: (compactKeep) => {
663
+ set({ compactKeep });
664
+ } }), _jsx(CompactionSummariserRow, { binding: binding, set: set }), _jsx(ChoiceRow, { label: "Counts", path: "models.*.compaction.meter", field: "compactMeter", isRequired: false, placeholder: fieldMeta('models.*.compaction.meter')?.unset, value: binding.compactMeter ?? '', options: catalogChoices('models.*.compaction.meter'), onChange: (compactMeter) => {
665
+ set({ compactMeter });
666
+ } })] }));
667
+ }
668
+ /** Compaction, where the agent summarises its older history, or another text agent does. */
669
+ function CompactionSection({ binding, set }) {
670
+ return (_jsxs(InspectorSection, { path: "models.*.compaction", note: sectionNote('compaction'), children: [_jsx(ChoiceRow, { label: "Compact", path: "models.*.compaction.timing", field: "compactTiming", isRequired: false, placeholder: fieldMeta('models.*.compaction')?.unset, value: binding.compactTiming ?? '', options: catalogChoices('models.*.compaction.timing'), onChange: (compactTiming) => {
671
+ set({
672
+ compactTiming,
673
+ compactMaxTokens: binding.compactMaxTokens ?? COMPACTION_DRAFT_DEFAULTS.compactMaxTokens,
674
+ compactAt: binding.compactAt ?? COMPACTION_DRAFT_DEFAULTS.compactAt,
675
+ compactKeep: binding.compactKeep ?? COMPACTION_DRAFT_DEFAULTS.compactKeep,
676
+ });
677
+ } }), binding.compactTiming && _jsx(CompactionRows, { binding: binding, set: set })] }));
678
+ }
679
+ /** A closed set's options as the kernel's catalog lists them, each with its own description. */
680
+ function catalogChoices(path) {
681
+ const meta = fieldMeta(path);
682
+ return (meta?.options ?? []).map((value) => ({
683
+ value: value,
684
+ description: meta?.optionDescriptions?.[value],
685
+ }));
686
+ }
687
+ /** The segment for a closed set left unset: the provider's default. */
688
+ const PROVIDER_DEFAULT = {
689
+ value: 'default',
690
+ label: 'Provider default',
691
+ icon: IconCircleDashed,
692
+ };
693
+ /** An unset pin (`''`) as the provider-default segment. */
694
+ function segmentOf(value) {
695
+ return value === '' ? 'default' : value;
696
+ }
697
+ /** The provider-default segment back to an unset pin. */
698
+ function pinOf(segment) {
699
+ return segment === 'default' ? '' : segment;
700
+ }
701
+ const BARGE_IN_SEGMENTS = [
702
+ PROVIDER_DEFAULT,
703
+ { value: 'START_OF_ACTIVITY_INTERRUPTS', label: 'Interrupts', icon: IconHandStop },
704
+ { value: 'NO_INTERRUPTION', label: 'No interruption', icon: IconHandOff },
705
+ ];
706
+ /** Gemini reads only a sensitivity's LOW or HIGH, so each end offers its own pair. */
707
+ const START_SENSITIVITY_SEGMENTS = [
708
+ PROVIDER_DEFAULT,
709
+ { value: 'START_SENSITIVITY_LOW', label: 'Low', icon: IconAntennaBars2 },
710
+ { value: 'START_SENSITIVITY_HIGH', label: 'High', icon: IconAntennaBars5 },
711
+ ];
712
+ const END_SENSITIVITY_SEGMENTS = [
713
+ PROVIDER_DEFAULT,
714
+ { value: 'END_SENSITIVITY_LOW', label: 'Low', icon: IconAntennaBars2 },
715
+ { value: 'END_SENSITIVITY_HIGH', label: 'High', icon: IconAntennaBars5 },
716
+ ];
717
+ /** Whether every model is on Google, so the Google preset's vocabularies apply to the pins. */
718
+ function allGoogle(draft) {
719
+ return (draft.modelBindings.length > 0 &&
720
+ draft.modelBindings.every((binding) => isGoogleRoute(modelRoute(draft.providers, draft.identity.profileType, binding))));
721
+ }
722
+ /**
723
+ * A pin the kernel takes as any string: the Google preset's values when every model is on
724
+ * Google, free text otherwise.
725
+ */
726
+ function PresetRow({ google, label, path, value, preset, hasSearch, onChange, }) {
727
+ return google ? (_jsx(ChoiceRow, { label: label, path: path, value: value, options: preset, hasSearch: hasSearch, onChange: onChange })) : (_jsx(TextRow, { label: label, path: path, value: value, onChange: onChange }));
728
+ }
729
+ /** OpenRouter's image vocabularies; the kernel takes free strings, so these are the studio's. */
730
+ const OPENROUTER_IMAGE_QUALITIES = ['auto', 'low', 'medium', 'high'];
731
+ const OPENROUTER_IMAGE_BACKGROUNDS = ['auto', 'transparent', 'opaque'];
732
+ /** A pinned file rides in every turn's request, so it stays small. */
733
+ const REFERENCE_MAX_BYTES = 4 * 1024 * 1024;
734
+ /** A picked file's type and its bytes in base64. */
735
+ async function fileData(file) {
736
+ const url = await new Promise((resolve, reject) => {
737
+ const reader = new FileReader();
738
+ reader.onload = () => {
739
+ resolve(typeof reader.result === 'string' ? reader.result : '');
740
+ };
741
+ reader.onerror = () => {
742
+ reject(reader.error ?? new Error('Couldn’t read the file.'));
743
+ };
744
+ reader.readAsDataURL(file);
745
+ });
746
+ return { mimeType: file.type, data: url.slice(url.indexOf(',') + 1) };
747
+ }
748
+ /** A picked image file as a pinned reference. */
749
+ async function referenceFromFile(file) {
750
+ return { key: draftKey('reference'), name: file.name, ...(await fileData(file)) };
751
+ }
752
+ /** An image's aspect ratio, resolution, and format. */
753
+ function ImageShapeRows({ image, set, google }) {
754
+ return (_jsxs(_Fragment, { children: [_jsx(PresetRow, { google: google, label: "Aspect ratio", path: "image.aspectRatio", value: image.aspectRatio, preset: GOOGLE_IMAGE_ASPECT_RATIOS, onChange: (aspectRatio) => {
755
+ set({ aspectRatio });
756
+ } }), _jsx(PresetRow, { google: google, label: "Resolution", path: "image.resolution", value: image.resolution, preset: GOOGLE_IMAGE_RESOLUTIONS, onChange: (resolution) => {
757
+ set({ resolution });
758
+ } }), _jsx(PresetRow, { google: google, label: "Format", path: "image.mimeType", value: image.mimeType, preset: GOOGLE_IMAGE_OUTPUT_MIMES, onChange: (mimeType) => {
759
+ set({ mimeType });
760
+ } })] }));
761
+ }
762
+ /** The pins only OpenRouter takes: quality, background, compression, and image count. */
763
+ function OpenRouterImageRows({ image, set }) {
764
+ return (_jsxs(_Fragment, { children: [_jsx(ChoiceRow, { label: "Quality", path: "image.quality", field: "quality", value: image.quality, options: OPENROUTER_IMAGE_QUALITIES, onChange: (quality) => {
765
+ set({ quality });
766
+ } }), _jsx(ChoiceRow, { label: "Background", path: "image.background", field: "background", value: image.background, options: OPENROUTER_IMAGE_BACKGROUNDS, onChange: (background) => {
767
+ set({ background });
768
+ } }), _jsx(NumberRow, { label: "Compression", path: "image.outputCompression", field: "outputCompression", value: image.outputCompression, min: 0, max: 100, isIntegerOnly: true, onChange: (outputCompression) => {
769
+ set({ outputCompression });
770
+ } }), _jsx(NumberRow, { label: "Images", path: "image.n", field: "n", units: "per turn", value: image.n, min: 1, max: 10, isIntegerOnly: true, onChange: (n) => {
771
+ set({ n });
772
+ } })] }));
773
+ }
774
+ /** Image output pins. OpenRouter-only pins show when every model is on OpenRouter. */
775
+ function ImageEditor({ draft, setDraft }) {
776
+ const google = allGoogle(draft);
777
+ const openRouter = draft.modelBindings.length > 0 &&
778
+ draft.modelBindings.every((binding) => isOpenRouterRoute(modelRoute(draft.providers, 'image', binding)));
779
+ const { image } = draft;
780
+ const set = patch(setDraft, 'image');
781
+ return (_jsxs(_Fragment, { children: [_jsxs(InspectorSection, { title: "Output", path: "image", children: [_jsx(ImageShapeRows, { image: image, set: set, google: google }), openRouter && _jsx(OpenRouterImageRows, { image: image, set: set }), _jsx(NumberRow, { label: "Seed", path: "image.seed", field: "seed", value: image.seed, min: 0, isIntegerOnly: true, onChange: (seed) => {
782
+ set({ seed });
783
+ } }), _jsx(SwitchRow, { label: "With text", path: "image.includeText", value: image.includeText, onChange: (includeText) => {
784
+ set({ includeText });
785
+ } })] }), _jsx(ImageReferencesEditor, { references: image.references, onChange: (references) => {
786
+ set({ references });
787
+ } })] }));
788
+ }
789
+ /** One reference: a pinned file's thumbnail and name, or a link's field, and a remove button. */
790
+ function ImageReferenceRow({ reference, index, onReplace, onRemove, }) {
791
+ const statusAt = useFieldStatus();
792
+ const label = `Reference ${String(index + 1)}`;
793
+ const status = statusAt('references', index);
794
+ return (_jsxs(InspectorRow, { label: label, path: "image.references", hasIssue: status !== undefined, children: ['data' in reference ? (_jsx(StackItem, { size: "fill", children: _jsxs(HStack, { gap: 2, vAlign: "center", children: [_jsx(Thumbnail, { src: `data:${reference.mimeType};base64,${reference.data}`, alt: reference.name, label: reference.name }), _jsx(Text, { size: "sm", maxLines: 1, children: reference.name })] }) })) : (_jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: `${label} link`, isLabelHidden: true, size: "sm", status: status, value: reference.uri, placeholder: "https://\u2026/image.png", onChange: (uri) => {
795
+ onReplace({ ...reference, uri });
796
+ } }) })), _jsx(IconButton, { label: `Remove ${label.toLowerCase()}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: onRemove })] }));
797
+ }
798
+ /** A dropzone's `changeAction`: the files picked, read while it shows as loading. */
799
+ function useFilesRead(read) {
800
+ const [reading, setReading] = useState(false);
801
+ const changeAction = async (files) => {
802
+ const picked = files === null ? [] : Array.isArray(files) ? files : [files];
803
+ if (!picked.length)
804
+ return;
805
+ setReading(true);
806
+ try {
807
+ await read(picked);
808
+ }
809
+ finally {
810
+ setReading(false);
811
+ }
812
+ };
813
+ return { reading, changeAction };
814
+ }
815
+ /** Reference images sent with every turn, ahead of what the user attaches: files or links. */
816
+ function ImageReferencesEditor({ references, onChange, }) {
817
+ const { reading, changeAction } = useFilesRead(async (picked) => {
818
+ onChange([...references, ...(await Promise.all(picked.map(referenceFromFile)))]);
819
+ });
820
+ const replace = (key, next) => {
821
+ onChange(references.map((reference) => (reference.key === key ? next : reference)));
822
+ };
823
+ return (_jsxs(InspectorSection, { title: "References", path: "image.references", children: [references.map((reference, index) => (_jsx(ImageReferenceRow, { reference: reference, index: index, onReplace: (next) => {
824
+ replace(reference.key, next);
825
+ }, onRemove: () => {
826
+ onChange(references.filter((candidate) => candidate.key !== reference.key));
827
+ } }, reference.key))), _jsx(FileInput, { label: "Add images", isLabelHidden: true, mode: "dropzone", accept: "image/*", isMultiple: true, maxSize: REFERENCE_MAX_BYTES, isLoading: reading, placeholder: "Drop images or choose files, up to 4 MB each", value: null, changeAction: changeAction, onChange: () => undefined }), _jsx(Button, { label: "Add link", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), onClick: () => {
828
+ onChange([...references, { key: draftKey('reference'), uri: '' }]);
829
+ } })] }));
830
+ }
831
+ /** Speech pins. mp3 is only on offer when every model's provider can make it. */
832
+ function SpeechEditor({ draft, setDraft }) {
833
+ const google = allGoogle(draft);
834
+ const { speech } = draft;
835
+ const set = patch(setDraft, 'speech');
836
+ // Gemini refuses mp3.
837
+ const mp3 = draft.modelBindings.every((binding) => !isGoogleRoute(modelRoute(draft.providers, 'speech', binding)));
838
+ return (_jsxs(InspectorSection, { path: "speech", children: [_jsx(PresetRow, { google: google, label: "Voice", path: "speech.voice", value: speech.voice, preset: GOOGLE_SPEECH_VOICES, hasSearch: true, onChange: (voice) => {
839
+ set({ voice });
840
+ } }), _jsx(TextAreaRow, { label: "Style", path: "speech.style", field: "style", value: speech.style, rows: 2, onChange: (style) => {
841
+ set({ style });
842
+ } }), _jsx(NumberRow, { label: "Speed", path: "speech.speed", field: "speed", value: speech.speed, step: 0.1, onChange: (speed) => {
843
+ set({ speed });
844
+ } }), _jsx(ChoiceRow, { label: "Format", path: "speech.format", field: "format", value: speech.format, options: [
845
+ 'pcm',
846
+ {
847
+ value: 'mp3',
848
+ label: 'mp3',
849
+ ...(mp3 ? {} : { description: 'Only openAi models make mp3', disabled: true }),
850
+ },
851
+ ], onChange: (format) => {
852
+ set({ format });
853
+ } })] }));
854
+ }
855
+ /** Compression sliders move in steps of 1,024 tokens, which divides the free key's cap. */
856
+ const COMPRESSION_STEP = 1024;
857
+ function LiveIngressSection({ live, set }) {
858
+ return (_jsxs(InspectorSection, { title: "Ingress", path: "live.ingress", children: [_jsx(SwitchRow, { label: "Audio", path: "live.ingress.audio", value: live.ingressAudio, onChange: (ingressAudio) => {
859
+ set({ ingressAudio });
860
+ } }), _jsx(SwitchRow, { label: "Video", path: "live.ingress.video", value: live.ingressVideo, onChange: (ingressVideo) => {
861
+ set({ ingressVideo });
862
+ } }), _jsx(SwitchRow, { label: "Text", path: "live.ingress.text", value: live.ingressText, onChange: (ingressText) => {
863
+ set({ ingressText });
864
+ } })] }));
865
+ }
866
+ function LiveVoiceSection({ live, set, google }) {
867
+ return (_jsx(InspectorSection, { path: "live.voice", children: _jsx(PresetRow, { google: google, label: "Voice", path: "live.voice", value: live.voice, preset: GOOGLE_SPEECH_VOICES, hasSearch: true, onChange: (voice) => {
868
+ set({ voice });
869
+ } }) }));
870
+ }
871
+ function LiveSessionSection({ live, set }) {
872
+ return (_jsxs(InspectorSection, { title: "Session", path: "live.sessionResumption", children: [_jsx(SwitchRow, { label: "Resumption", path: "live.sessionResumption", value: live.sessionResumption, onChange: (sessionResumption) => {
873
+ set({ sessionResumption });
874
+ } }), live.sessionResumption && (_jsx(TextAreaRow, { label: "On resume", path: "live.resumed.prompt", field: "resumedPrompt", value: live.resumedPrompt, rows: 2, onChange: (resumedPrompt) => {
875
+ set({ resumedPrompt });
876
+ } })), live.sessionResumption && live.resumedPrompt.trim() !== '' && (_jsx(NumberRow, { label: "Away for", path: "live.resumed.afterMs", units: "ms", field: "resumedAfterMs", value: live.resumedAfterMs, min: 0, isIntegerOnly: true, onChange: (resumedAfterMs) => {
877
+ set({ resumedAfterMs });
878
+ } }))] }));
879
+ }
880
+ /** What the agent is told to say when a call opens. */
881
+ function LiveGreetingSection({ live, set }) {
882
+ return (_jsx(InspectorSection, { path: "live.greeting", children: _jsx(TextAreaRow, { label: "Greeting", path: "live.greeting", field: "greeting", value: live.greeting, rows: 2, onChange: (greeting) => {
883
+ set({ greeting });
884
+ } }) }));
885
+ }
886
+ /** The sliding window: on lent keys its sizes are sliders, capped at what a session holds on them. */
887
+ function LiveCompressionSection({ live, set }) {
888
+ const keys = useLentKeys();
889
+ return (_jsxs(InspectorSection, { path: "live.contextCompression", children: [_jsx(SwitchRow, { label: "Sliding window", path: "live.contextCompression", value: live.contextCompression, onChange: (contextCompression) => {
890
+ set({ contextCompression });
891
+ } }), live.contextCompression &&
892
+ (keys ? (_jsx(CompressionSliderRows, { live: live, set: set, keys: keys })) : (_jsx(CompressionNumberRows, { live: live, set: set })))] }));
893
+ }
894
+ /** A blank size sits where a run on the keys puts it. */
895
+ function CompressionSliderRows({ live, set, keys }) {
896
+ const held = keys.liveInputTokens;
897
+ const fallback = lentCompression(keys, {
898
+ triggerTokens: live.compressionTriggerTokens ?? undefined,
899
+ targetTokens: live.compressionTargetTokens ?? undefined,
900
+ });
901
+ return (_jsxs(_Fragment, { children: [_jsx(SliderRow, { label: "Trigger", path: "live.contextCompression.triggerTokens", field: "compressionTriggerTokens", value: live.compressionTriggerTokens, fallback: fallback.triggerTokens, min: COMPRESSION_STEP, max: held, step: COMPRESSION_STEP, onChange: (compressionTriggerTokens) => {
902
+ set({ compressionTriggerTokens });
903
+ } }), _jsx(SliderRow, { label: "Keep", path: "live.contextCompression.slidingWindow.targetTokens", field: "compressionTargetTokens", value: live.compressionTargetTokens, fallback: fallback.targetTokens, min: COMPRESSION_STEP, max: held - COMPRESSION_STEP, step: COMPRESSION_STEP, onChange: (compressionTargetTokens) => {
904
+ set({ compressionTargetTokens });
905
+ } })] }));
906
+ }
907
+ function CompressionNumberRows({ live, set }) {
908
+ return (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Trigger", path: "live.contextCompression.triggerTokens", field: "compressionTriggerTokens", value: live.compressionTriggerTokens, min: 1, isIntegerOnly: true, onChange: (compressionTriggerTokens) => {
909
+ set({ compressionTriggerTokens });
910
+ } }), _jsx(NumberRow, { label: "Keep", path: "live.contextCompression.slidingWindow.targetTokens", field: "compressionTargetTokens", value: live.compressionTargetTokens, min: 1, isIntegerOnly: true, onChange: (compressionTargetTokens) => {
911
+ set({ compressionTargetTokens });
912
+ } })] }));
913
+ }
914
+ function LiveTranscriptsSection({ live, set }) {
915
+ return (_jsxs(InspectorSection, { title: "Transcripts", path: "live.transcription", children: [_jsx(SwitchRow, { label: "Input", path: "live.transcription.input", value: live.transcriptionInput, onChange: (transcriptionInput) => {
916
+ set({ transcriptionInput });
917
+ } }), _jsx(SwitchRow, { label: "Output", path: "live.transcription.output", value: live.transcriptionOutput, onChange: (transcriptionOutput) => {
918
+ set({ transcriptionOutput });
919
+ } })] }));
920
+ }
921
+ function LiveVadSection({ live, set }) {
922
+ return (_jsxs(InspectorSection, { title: "Voice activity", path: "live.vad", children: [_jsx(SegmentedRow, { label: "Barge-in", path: "live.vad.activityHandling", value: segmentOf(live.vadActivityHandling), segments: BARGE_IN_SEGMENTS, onChange: (segment) => {
923
+ set({ vadActivityHandling: pinOf(segment) });
924
+ } }), _jsx(SegmentedRow, { label: "Start", path: "live.vad.startSensitivity", value: segmentOf(live.vadStartSensitivity), segments: START_SENSITIVITY_SEGMENTS, onChange: (segment) => {
925
+ set({ vadStartSensitivity: pinOf(segment) });
926
+ } }), _jsx(SegmentedRow, { label: "End", path: "live.vad.endSensitivity", value: segmentOf(live.vadEndSensitivity), segments: END_SENSITIVITY_SEGMENTS, onChange: (segment) => {
927
+ set({ vadEndSensitivity: pinOf(segment) });
928
+ } }), _jsx(NumberRow, { label: "Padding", path: "live.vad.prefixPaddingMs", field: "vadPrefixPaddingMs", value: live.vadPrefixPaddingMs, min: 0, units: "ms", isIntegerOnly: true, onChange: (vadPrefixPaddingMs) => {
929
+ set({ vadPrefixPaddingMs });
930
+ } }), _jsx(NumberRow, { label: "Silence", path: "live.vad.silenceDurationMs", field: "vadSilenceDurationMs", value: live.vadSilenceDurationMs, min: 0, units: "ms", isIntegerOnly: true, onChange: (vadSilenceDurationMs) => {
931
+ set({ vadSilenceDurationMs });
932
+ } })] }));
933
+ }
934
+ function LiveEditor({ draft, setDraft }) {
935
+ const google = allGoogle(draft);
936
+ const { live } = draft;
937
+ const set = patch(setDraft, 'live');
938
+ return (_jsxs(_Fragment, { children: [_jsx(LiveIngressSection, { live: live, set: set }), _jsx(LiveVoiceSection, { live: live, set: set, google: google }), _jsx(LiveGreetingSection, { live: live, set: set }), _jsx(LiveSessionSection, { live: live, set: set }), _jsx(LiveCompressionSection, { live: live, set: set }), _jsx(LiveTranscriptsSection, { live: live, set: set }), _jsx(LiveVadSection, { live: live, set: set })] }));
939
+ }
940
+ const OUTPUT_MODE_SEGMENTS = [
941
+ { value: 'text', label: 'Free text', icon: IconAlignLeft },
942
+ { value: 'structured', label: 'Structured', icon: IconBraces },
943
+ ];
944
+ /** The kernel streams over SSE when the mode is left out, so SSE stands for the unset pin. */
945
+ const STREAM_MODE_SEGMENTS = [
946
+ { value: 'sse', label: 'Streamed', icon: IconWaveSine },
947
+ { value: 'buffered', label: 'Buffered', icon: IconPackage },
948
+ ];
949
+ const SCHEMA_PLACEHOLDER = `{
950
+ "type": "object",
951
+ "properties": { "answer": { "type": "string" } },
952
+ "required": ["answer"]
953
+ }`;
954
+ /** Free text or structured, and a structured reply's schema. */
955
+ function OutputShapeSection({ outputs, set, structured, }) {
956
+ return (_jsxs(InspectorSection, { title: "Shape", path: "outputs.structured", children: [_jsx(SegmentedRow, { label: "Mode", path: "outputs.structured", value: outputs.mode, segments: OUTPUT_MODE_SEGMENTS, onChange: (mode) => {
957
+ set({ mode });
958
+ } }), structured && (_jsxs(_Fragment, { children: [_jsx(TextRow, { label: "Schema id", path: "outputs.structured", field: "schemaId", isRequired: true, value: outputs.schemaId, placeholder: "reply", onChange: (schemaId) => {
959
+ set({ schemaId });
960
+ } }), _jsx(TextAreaRow, { label: "JSON Schema", path: "outputs.structured", field: "schemaJson", isRequired: true, rows: 8, hasSpellCheck: false, value: outputs.schemaJson, placeholder: SCHEMA_PLACEHOLDER, onChange: (schemaJson) => {
961
+ set({ schemaJson });
962
+ } })] }))] }));
963
+ }
964
+ /** What a list's check does with an item it refuses. The check is the project's code. */
965
+ const ITEM_RULE_SEGMENTS = [
966
+ { value: 'drop', label: 'Remove', icon: IconTrash },
967
+ { value: 'fix', label: 'Rewrite', icon: IconRefresh },
968
+ ];
969
+ function OutputRepairSection({ outputs, set }) {
970
+ return (_jsxs(InspectorSection, { title: "Repair", path: "outputs.validation", children: [_jsx(SwitchRow, { label: "Repair", path: "outputs.validation", value: outputs.validationEnabled, onChange: (validationEnabled) => {
971
+ set({ validationEnabled });
972
+ } }), outputs.validationEnabled && (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Retries", path: "outputs.validation.maxRetries", units: "retries", field: "maxRetries", value: outputs.maxRetries, min: 0, isIntegerOnly: true, onChange: (maxRetries) => {
973
+ set({ maxRetries });
974
+ } }), Object.entries(outputs.itemRules).map(([list, onFail]) => (_jsx(SegmentedRow, { label: `A refused item of ${list}`, path: "outputs.validation.items.*.onFail", value: onFail, segments: ITEM_RULE_SEGMENTS, onChange: (next) => {
975
+ set({ itemRules: { ...outputs.itemRules, [list]: next } });
976
+ } }, list))), _jsx(TextAreaRow, { label: "Guidance", path: "labels.repair.default_guidance", field: "repairGuidance", value: outputs.repairGuidance, placeholder: labelDefault('repair.default_guidance'), onChange: (repairGuidance) => {
977
+ set({ repairGuidance });
978
+ } })] }))] }));
979
+ }
980
+ function OutputsEditor({ draft, setDraft }) {
981
+ const { outputs } = draft;
982
+ const set = patch(setDraft, 'outputs');
983
+ const shaped = draftAllows(draft, 'outputs.structured');
984
+ const structured = shaped && outputs.mode === 'structured';
985
+ return (_jsxs(_Fragment, { children: [shaped && _jsx(OutputShapeSection, { outputs: outputs, set: set, structured: structured }), structured && _jsx(OutputRepairSection, { outputs: outputs, set: set }), _jsxs(InspectorSection, { title: "Streaming", path: "outputs.streaming", children: [_jsx(SegmentedRow, { label: "Delivery", path: "outputs.streaming.mode", value: outputs.streamMode || 'sse', segments: STREAM_MODE_SEGMENTS, onChange: (mode) => {
986
+ set({ streamMode: mode === 'sse' ? '' : mode });
987
+ } }), _jsx(SwitchRow, { label: "Thoughts", path: "outputs.streaming.streamThoughts", value: outputs.streamThoughts, onChange: (streamThoughts) => {
988
+ set({ streamThoughts });
989
+ } })] })] }));
990
+ }
991
+ const STOP_KIND_LABEL = {
992
+ length: 'Length',
993
+ stream_incomplete: 'Dropped stream',
994
+ provider_error: 'Provider error',
995
+ };
996
+ /** The picks after one box of the grid changes. The two columns are picked apart. */
997
+ function pickKind(current, kind, column, checked) {
998
+ const set = (kinds) => CONTINUE_STOP_KINDS.filter((candidate) => candidate === kind ? checked : kinds.includes(candidate));
999
+ return column === 'offer'
1000
+ ? { ...current, allow: set(current.allow) }
1001
+ : { ...current, auto: set(current.auto) };
1002
+ }
1003
+ /** What a picked stop does, for its token's hover. */
1004
+ function resumptionSays({ allow, auto }, kind) {
1005
+ if (!auto.includes(kind))
1006
+ return 'Offers Continue';
1007
+ return allow.includes(kind)
1008
+ ? 'Continues on its own, and offers Continue'
1009
+ : 'Continues on its own';
1010
+ }
1011
+ /**
1012
+ * The trigger's tokens: each stop picked in either column, a bolt on the ones that continue on
1013
+ * their own, the first few shown and the rest counted, as the list rows do.
1014
+ */
1015
+ function ResumptionTokens({ allow, auto }) {
1016
+ const maxBadges = useContext(ListBadges);
1017
+ const picked = CONTINUE_STOP_KINDS.filter((kind) => allow.includes(kind) || auto.includes(kind));
1018
+ const rest = picked.length - maxBadges;
1019
+ return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [picked.slice(0, maxBadges).map((kind) => (_jsx(Token, { size: "sm", label: STOP_KIND_LABEL[kind], icon: auto.includes(kind) ? _jsx(Icon, { icon: IconBolt, size: "sm" }) : undefined, description: resumptionSays({ allow, auto }, kind) }, kind))), rest > 0 && _jsx(Token, { size: "sm", label: `+${String(rest)}` })] }));
1020
+ }
1021
+ /** One box of the grid: whether a stop kind is offered, or continues on its own. */
1022
+ function ResumptionBox({ current, change, kind, column, }) {
1023
+ return (_jsx(CheckboxInput, { label: `${column === 'offer' ? 'Offer Continue' : 'Continue on its own'} after ${STOP_KIND_LABEL[kind].toLowerCase()}`, isLabelHidden: true, value: (column === 'offer' ? current.allow : current.auto).includes(kind), onChange: (checked) => {
1024
+ change(pickKind(current, kind, column, checked));
1025
+ } }));
1026
+ }
1027
+ /** The picker's grid: a row per stop kind, with an Offer box and an Auto box. */
1028
+ function ResumptionGrid({ current, change }) {
1029
+ const kinds = fieldMeta('turnBehaviour.resumption.allowContinue')?.optionDescriptions;
1030
+ const offerDoc = fieldMeta('turnBehaviour.resumption.allowContinue')?.doc;
1031
+ const autoDoc = fieldMeta('turnBehaviour.resumption.autoContinue')?.doc;
1032
+ const box = (kind, column) => (_jsx(ResumptionBox, { current: current, change: change, kind: kind, column: column }));
1033
+ return (_jsx(VStack, { width: 280, padding: 2, children: _jsx(Table, { density: "compact", dividers: "rows", idKey: "kind", data: CONTINUE_STOP_KINDS.map((kind) => ({ kind })), columns: [
1034
+ {
1035
+ key: 'kind',
1036
+ header: 'After',
1037
+ width: proportional(1),
1038
+ renderCell: ({ kind }) => (_jsx(Tooltip, { content: kinds?.[kind], hasHoverIndication: false, children: STOP_KIND_LABEL[kind] })),
1039
+ },
1040
+ {
1041
+ key: 'offer',
1042
+ header: (_jsx(Tooltip, { content: offerDoc, hasHoverIndication: false, children: "Offer" })),
1043
+ width: pixel(64),
1044
+ align: 'center',
1045
+ renderCell: ({ kind }) => box(kind, 'offer'),
1046
+ },
1047
+ {
1048
+ key: 'auto',
1049
+ header: (_jsx(Tooltip, { content: autoDoc, hasHoverIndication: false, children: "Auto" })),
1050
+ width: pixel(64),
1051
+ align: 'center',
1052
+ renderCell: ({ kind }) => box(kind, 'auto'),
1053
+ },
1054
+ ] }) }));
1055
+ }
1056
+ /**
1057
+ * Resumption as one picker, a checklist per stop kind. Picking anything turns resumption on, and
1058
+ * clearing everything turns it off, so there is no separate switch.
1059
+ */
1060
+ function ResumptionRow({ value, onChange, }) {
1061
+ return (_jsx(InspectorRow, { label: "Continue after", path: "turnBehaviour.resumption", children: _jsx(StackItem, { size: "fill", children: _jsx(ComplexSelector, { label: "Continue after", isLabelHidden: true, size: "sm", placement: "below", value: value, triggerLabel: value.allow.length || value.auto.length ? _jsx(ResumptionTokens, { ...value }) : undefined, placeholder: "Never", onChange: onChange, children: (current, change) => _jsx(ResumptionGrid, { current: current, change: change }) }) }) }));
1062
+ }
1063
+ function ResumptionSection({ draft, set, }) {
1064
+ const turn = draft.turnBehaviour;
1065
+ return (_jsxs(InspectorSection, { title: "Resumption", path: "turnBehaviour.resumption", children: [_jsx(ResumptionRow, { value: turn.resumeEnabled
1066
+ ? { allow: turn.allowContinue, auto: turn.autoContinue }
1067
+ : { allow: [], auto: [] }, onChange: ({ allow, auto }) => {
1068
+ set({
1069
+ resumeEnabled: allow.length > 0 || auto.length > 0,
1070
+ allowContinue: allow,
1071
+ autoContinue: auto,
1072
+ });
1073
+ } }), turn.resumeEnabled && (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Max rounds", path: "turnBehaviour.resumption.maxContinues", units: "rounds", field: "maxContinues", value: turn.maxContinues, min: 1, isIntegerOnly: true, onChange: (maxContinues) => {
1074
+ set({ maxContinues });
1075
+ } }), takesContinueInstruction(draft) && (_jsx(TextAreaRow, { label: "Instruction", path: "labels.continue.instruction", field: "continueInstruction", value: turn.continueInstruction, placeholder: labelDefault('continue.instruction'), onChange: (continueInstruction) => {
1076
+ set({ continueInstruction });
1077
+ } }))] }))] }));
1078
+ }
1079
+ const STEP_LIMIT_SEGMENTS = [
1080
+ { value: 'stop', label: 'Stop', icon: IconHandStop },
1081
+ { value: 'answer', label: 'Answer', icon: IconMessage },
1082
+ ];
1083
+ function TurnBehaviourEditor({ draft, setDraft }) {
1084
+ const turn = draft.turnBehaviour;
1085
+ const set = patch(setDraft, 'turnBehaviour');
1086
+ return (_jsxs(_Fragment, { children: [draftAllows(draft, 'turnBehaviour.resumption') && (_jsx(ResumptionSection, { draft: draft, set: set })), draftAllows(draft, 'turnBehaviour.allowSteering') && (_jsx(InspectorSection, { path: "turnBehaviour.allowSteering", children: _jsx(SwitchRow, { label: "Steering", path: "turnBehaviour.allowSteering", value: turn.allowSteering, onChange: (allowSteering) => {
1087
+ set({ allowSteering });
1088
+ } }) })), draftAllows(draft, 'turnBehaviour.atStepLimit') && (_jsx(InspectorSection, { path: "turnBehaviour.atStepLimit", children: _jsx(SegmentedRow, { label: "At the step limit", path: "turnBehaviour.atStepLimit", value: turn.atStepLimit, segments: STEP_LIMIT_SEGMENTS, onChange: (atStepLimit) => {
1089
+ set({ atStepLimit });
1090
+ } }) }))] }));
1091
+ }
1092
+ /** The kernel retries when on-block is left out, so retry stands for the unset pin. */
1093
+ const ON_BLOCK_SEGMENTS = [
1094
+ { value: 'retry', label: 'Retry', icon: IconRefresh },
1095
+ { value: 'refuse', label: 'Refuse', icon: IconHandStop },
1096
+ ];
1097
+ /** Each threshold wears the icon of the tool access it starts refusing; none is the guard off. */
1098
+ const TAINT_SEGMENTS = [
1099
+ { value: 'off', label: 'None', icon: IconShieldOff },
1100
+ { value: 'destructive', label: 'Destructive', icon: IconFlame },
1101
+ { value: 'write', label: 'Writes', icon: IconPencil },
1102
+ ];
1103
+ const DESTINATION_SEGMENTS = [
1104
+ { value: 'off', label: 'Log', icon: IconFlag },
1105
+ { value: 'confirm', label: 'Ask', icon: IconUserCheck },
1106
+ { value: 'block', label: 'Refuse', icon: IconBan },
1107
+ ];
1108
+ /** The addresses an image or link detector lets through besides the ones the model was given. */
1109
+ function UrlAllowRows({ detector, allow, onChange, }) {
1110
+ return (_jsxs(_Fragment, { children: [_jsx(NamesRow, { label: "Allowed hosts", path: `guardrails.detect.${detector}.allow.hosts`, field: `allow.${detector}.hosts`, value: allow.hosts, onChange: (hosts) => {
1111
+ onChange({ ...allow, hosts });
1112
+ } }), _jsx(SwitchRow, { label: "Tool URLs", path: `guardrails.detect.${detector}.allow.fromTools`, value: allow.fromTools, onChange: (fromTools) => {
1113
+ onChange({ ...allow, fromTools });
1114
+ } })] }));
1115
+ }
1116
+ /** The note that binds the canary, on the canary leak detector's page while it reads somewhere. */
1117
+ function CanarySection({ guardrails, set }) {
1118
+ return (_jsx(InspectorSection, { children: _jsx(TextAreaRow, { label: "Bind note", path: "labels.canary.bind_note", field: "canaryBindNote", value: guardrails.canaryBindNote, placeholder: labelDefault('canary.bind_note'), onChange: (canaryBindNote) => {
1119
+ set({ canaryBindNote });
1120
+ } }) }));
1121
+ }
1122
+ /**
1123
+ * Retries and guidance, shown when a blocked reply goes back to the agent. At no retries nothing goes
1124
+ * back, so the row says what the visitor reads in place of the reply, and guidance has no reader.
1125
+ */
1126
+ function BlockedReplyRetryRows({ guardrails, withheld, set, }) {
1127
+ const isNever = guardrails.blockedReplyMaxRetries === 0;
1128
+ return (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Retries", path: "guardrails.blockedReply.maxRetries", units: "retries", field: "blockedReplyMaxRetries", value: guardrails.blockedReplyMaxRetries, min: 0, isIntegerOnly: true, status: isNever
1129
+ ? {
1130
+ type: 'warning',
1131
+ message: `At 0 a blocked reply is not rewritten. The visitor reads: \u201c${withheld}\u201d`,
1132
+ }
1133
+ : undefined, onChange: (blockedReplyMaxRetries) => {
1134
+ set({ blockedReplyMaxRetries });
1135
+ } }), !isNever && (_jsx(TextAreaRow, { label: "Guidance", path: "labels.egress.default_repair_guidance", field: "egressRepairGuidance", value: guardrails.egressRepairGuidance, placeholder: labelDefault('egress.default_repair_guidance'), onChange: (egressRepairGuidance) => {
1136
+ set({ egressRepairGuidance });
1137
+ } }))] }));
1138
+ }
1139
+ /** What happens to a reply a detector blocks. */
1140
+ function BlockedReplySection({ guardrails, withheld, set, }) {
1141
+ return (_jsxs(InspectorSection, { path: "guardrails.blockedReply", children: [_jsx(SegmentedRow, { label: "On block", path: "guardrails.blockedReply.onBlock", value: guardrails.blockedReplyOnBlock || 'retry', segments: ON_BLOCK_SEGMENTS, onChange: (onBlock) => {
1142
+ set({ blockedReplyOnBlock: onBlock === 'retry' ? '' : onBlock });
1143
+ } }), guardrails.blockedReplyOnBlock === 'refuse' ? (_jsx(TextAreaRow, { label: "Message", path: "labels.egress.refusal", field: "egressRefusal", value: guardrails.egressRefusal, placeholder: labelDefault('egress.refusal'), onChange: (egressRefusal) => {
1144
+ set({ egressRefusal });
1145
+ } })) : (_jsx(BlockedReplyRetryRows, { guardrails: guardrails, withheld: withheld, set: set }))] }));
1146
+ }
1147
+ function NetworkSection({ guardrails, set }) {
1148
+ const on = useContext(RunsOn);
1149
+ const runtime = useContext(LocalConnection)?.runtime ?? on;
1150
+ return (_jsxs(InspectorSection, { title: "Network", path: "guardrails.network", note: studioNetworkNote(runtime), children: [_jsx(SwitchRow, { label: "Private", path: "guardrails.network.allowPrivateNetworks", value: guardrails.allowPrivateNetworks, onChange: (allowPrivateNetworks) => {
1151
+ set({ allowPrivateNetworks });
1152
+ } }), _jsx(NamesRow, { label: "Allow hosts", path: "guardrails.network.allowedHosts", field: "allowedHosts", value: guardrails.allowedHosts, placeholder: "None: private hosts stay blocked", isDisabled: guardrails.allowPrivateNetworks, disabledMessage: "Every private host is already allowed.", onChange: (allowedHosts) => {
1153
+ set({ allowedHosts });
1154
+ } }), _jsx(NamesRow, { label: "Schemes", path: "guardrails.network.allowedSchemes", field: "allowedSchemes", value: guardrails.allowedSchemes, onChange: (allowedSchemes) => {
1155
+ set({ allowedSchemes });
1156
+ } })] }));
1157
+ }
1158
+ function QuotaSection({ guardrails, set }) {
1159
+ return (_jsxs(InspectorSection, { title: "Quota", path: "guardrails.quota", note: sectionNote('quota'), children: [_jsx(SwitchRow, { label: "Daily cap", path: "guardrails.quota", value: guardrails.quotaEnabled, onChange: (quotaEnabled) => {
1160
+ set({ quotaEnabled });
1161
+ } }), guardrails.quotaEnabled && (_jsxs(_Fragment, { children: [_jsx(NumberRow, { label: "Per day", path: "guardrails.quota.perDay", field: "quotaPerDay", isRequired: true, value: guardrails.quotaPerDay, min: 1, units: "turns", isIntegerOnly: true, onChange: (quotaPerDay) => {
1162
+ set({ quotaPerDay });
1163
+ } }), _jsx(TextAreaRow, { label: "Message", path: "labels.quota.exhausted", field: "quotaMessage", value: guardrails.quotaMessage, placeholder: labelDefault('quota.exhausted', {
1164
+ perDay: guardrails.quotaPerDay ?? '{perDay}',
1165
+ }), onChange: (quotaMessage) => {
1166
+ set({ quotaMessage });
1167
+ } })] }))] }));
1168
+ }
1169
+ /** Every action, so one the kernel adds fails the type check until it has an icon. */
1170
+ const DETECT_ACTION_ICON = {
1171
+ ignore: IconEyeOff,
1172
+ flag: IconFlag,
1173
+ redact: IconEraser,
1174
+ block: IconBan,
1175
+ };
1176
+ /** Every action, with the one the kernel recommends marked. */
1177
+ function detectSegments(recommended) {
1178
+ return DETECT_ACTIONS.map((action) => ({
1179
+ value: action,
1180
+ label: DETECT_ACTION_META[action].label,
1181
+ icon: DETECT_ACTION_ICON[action],
1182
+ isRecommended: action === recommended,
1183
+ }));
1184
+ }
1185
+ /** "Mixed": the pick a detector shows while its boundaries differ. Offered only then, already picked. */
1186
+ const MIXED_SEGMENT = {
1187
+ value: 'mixed',
1188
+ label: 'Mixed',
1189
+ icon: IconAdjustmentsHorizontal,
1190
+ description: sectionNote('detect.mixed'),
1191
+ };
1192
+ const TOOL_KIND_LABEL = {
1193
+ function: 'Function',
1194
+ http: 'HTTP',
1195
+ mcp: 'MCP',
1196
+ agent: 'Agent',
1197
+ };
1198
+ /** Where a tool's text crosses, with the boundary each kind of tool has there. */
1199
+ const TOOL_CROSSINGS = [
1200
+ { title: 'Tool arguments', boundary: (kind) => `tool_arguments_${kind}` },
1201
+ { title: 'Tool output', boundary: (kind) => `tool_output_${kind}` },
1202
+ { title: 'Tool errors', boundary: (kind) => `tool_failure_${kind}` },
1203
+ { title: 'Shown from tools', boundary: (kind) => `tool_shown_${kind}` },
1204
+ ];
1205
+ const TOOL_BOUNDARY_SET = new Set(TOOL_BOUNDARIES);
1206
+ /** One boundary of one detector: what happens to a match there. */
1207
+ function BoundaryRow({ label, detector, boundary, actions, recommended, onChange, }) {
1208
+ return (_jsx(SegmentedRow, { label: label, path: `guardrails.detect.${detector}.at.${boundary}`, value: actions[boundary], segments: detectSegments(recommended), onChange: (action) => {
1209
+ onChange({ ...actions, [boundary]: action });
1210
+ } }));
1211
+ }
1212
+ /**
1213
+ * One control for a detector's every boundary: it shows the action they share, or "Mixed" when
1214
+ * they differ, and a pick sets them all.
1215
+ */
1216
+ function ActionRow({ detector, label, defaults, boundaries, actions, status, trailing, onChange, }) {
1217
+ const taken = [...new Set(boundaries.map((boundary) => actions[boundary]))];
1218
+ const uniform = taken.length === 1;
1219
+ // One action is recommended for the whole detector only when every boundary recommends the same.
1220
+ const recommended = [...new Set(boundaries.map((boundary) => defaults?.[boundary]))];
1221
+ const segments = detectSegments(recommended.length === 1 ? recommended[0] : undefined);
1222
+ return (_jsx(SegmentedRow, { label: label, path: `guardrails.detect.${detector}.action`, value: uniform ? taken[0] : 'mixed', segments: uniform ? segments : [...segments, MIXED_SEGMENT], status: status, trailing: trailing, onChange: (action) => {
1223
+ if (action === 'mixed')
1224
+ return;
1225
+ const next = { ...actions };
1226
+ for (const boundary of boundaries)
1227
+ next[boundary] = action;
1228
+ onChange(next);
1229
+ } }));
1230
+ }
1231
+ /** Every boundary of one detector, in the kernel's order, a tool's under the crossing it is. */
1232
+ function BoundaryRows({ detector, defaults, boundaries, actions, onChange, }) {
1233
+ const has = new Set(boundaries);
1234
+ // The boundaries that are not a tool's, which sit either side of the tool ones in the kernel's order.
1235
+ const plain = boundaries.filter((boundary) => !TOOL_BOUNDARY_SET.has(boundary));
1236
+ const firstTool = BOUNDARIES.findIndex((boundary) => TOOL_BOUNDARY_SET.has(boundary));
1237
+ const row = (boundary, label) => (_jsx(BoundaryRow, { label: label, detector: detector, boundary: boundary, actions: actions, recommended: defaults?.[boundary], onChange: onChange }, boundary));
1238
+ return (_jsxs(_Fragment, { children: [plain
1239
+ .filter((boundary) => BOUNDARIES.indexOf(boundary) < firstTool)
1240
+ .map((boundary) => row(boundary, BOUNDARY_META[boundary].label)), TOOL_CROSSINGS.filter((crossing) => TOOL_KINDS.some((kind) => has.has(crossing.boundary(kind)))).map((crossing) => (_jsxs(VStack, { gap: 2, children: [_jsx(InspectorGroupTitle, { title: crossing.title }), TOOL_KINDS.filter((kind) => has.has(crossing.boundary(kind))).map((kind) => row(crossing.boundary(kind), TOOL_KIND_LABEL[kind]))] }, crossing.title))), plain
1241
+ .filter((boundary) => BOUNDARIES.indexOf(boundary) > firstTool)
1242
+ .map((boundary) => row(boundary, BOUNDARY_META[boundary].label))] }));
1243
+ }
1244
+ /**
1245
+ * What a detector does with a match: one control for every boundary, and each boundary's own
1246
+ * under it, shut until opened. It starts open while the boundaries differ.
1247
+ */
1248
+ function ActionSection({ detector, defaults, boundaries, actions, status, onChange, }) {
1249
+ const uniform = new Set(boundaries.map((boundary) => actions[boundary])).size === 1;
1250
+ return (_jsxs(InspectorSection, { children: [_jsx(ActionRow, { detector: detector, label: "On a match", defaults: defaults, boundaries: boundaries, actions: actions, status: status, onChange: onChange }), boundaries.length > 1 && (_jsx(CollapsibleGroup, { type: "multiple", density: "compact", defaultValue: uniform ? [] : ['each'], children: _jsx(Collapsible, { value: "each", trigger: _jsx(Text, { type: "supporting", children: "Each boundary" }), children: _jsx(VStack, { gap: 3, children: _jsx(BoundaryRows, { detector: detector, defaults: defaults, boundaries: boundaries, actions: actions, onChange: onChange }) }) }) }))] }));
1251
+ }
1252
+ const PATTERN_KIND_SEGMENTS = [
1253
+ { value: 'words', label: 'Words', icon: IconAbc },
1254
+ { value: 'pattern', label: 'Regular expression', icon: IconRegex },
1255
+ ];
1256
+ /** The flags a regular expression takes, each by what it does. */
1257
+ const REGEX_FLAGS = [
1258
+ { flag: 'i', label: 'Ignore case' },
1259
+ { flag: 'm', label: '^ and $ match each line' },
1260
+ { flag: 's', label: '. matches line breaks' },
1261
+ { flag: 'u', label: 'Unicode' },
1262
+ ];
1263
+ /** A regular expression's flags as a checklist: the ones set are checked. */
1264
+ function RegexFlagsRow({ detector, flags, onChange, }) {
1265
+ return (_jsx(InspectorRow, { label: "Options", path: `guardrails.detect.${detector}.patterns.*.flags`, children: _jsx(StackItem, { size: "fill", children: _jsx(CheckboxList, { label: "Options", isLabelHidden: true, density: "compact", value: REGEX_FLAGS.filter(({ flag }) => flags.includes(flag)).map(({ flag }) => flag), onChange: (checked) => {
1266
+ onChange(REGEX_FLAGS.filter(({ flag }) => checked.includes(flag))
1267
+ .map(({ flag }) => flag)
1268
+ .join(''));
1269
+ }, children: REGEX_FLAGS.map(({ flag, label }) => (_jsx(CheckboxListItem, { value: flag, label: label }, flag))) }) }) }));
1270
+ }
1271
+ /** One of the builder's patterns: its name, and the words or the regular expression it matches. */
1272
+ function PatternRows({ detector, pattern, index, status, onChange, onRemove, }) {
1273
+ const remove = `Remove ${pattern.name.trim() || `pattern ${String(index + 1)}`}`;
1274
+ return (_jsxs(VStack, { gap: 2, children: [_jsxs(InspectorRow, { label: "Name", path: `guardrails.detect.${detector}.patterns.*.name`, hasIssue: status !== undefined, children: [_jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: `Pattern ${String(index + 1)} name`, isLabelHidden: true, size: "sm", status: status, value: pattern.name, placeholder: "codenames", onChange: (name) => {
1275
+ onChange({ ...pattern, name });
1276
+ } }) }), _jsx(Tooltip, { content: remove, children: _jsx(IconButton, { label: remove, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: onRemove }) })] }), _jsx(SegmentedRow, { label: "Matches", path: `guardrails.detect.${detector}.patterns`, value: pattern.kind, segments: PATTERN_KIND_SEGMENTS, onChange: (kind) => {
1277
+ onChange({ ...pattern, kind });
1278
+ } }), pattern.kind === 'words' ? (_jsx(NamesRow, { label: "Words", path: `guardrails.detect.${detector}.patterns.*.words`, value: pattern.words, placeholder: "Bluebird", onChange: (words) => {
1279
+ onChange({ ...pattern, words });
1280
+ } })) : (_jsxs(_Fragment, { children: [_jsx(TextRow, { label: "Expression", path: `guardrails.detect.${detector}.patterns.*.pattern`, value: pattern.pattern, hint: "ORD-\\d{6}", onChange: (source) => {
1281
+ onChange({ ...pattern, pattern: source });
1282
+ } }), _jsx(RegexFlagsRow, { detector: detector, flags: pattern.flags, onChange: (flags) => {
1283
+ onChange({ ...pattern, flags });
1284
+ } })] }))] }));
1285
+ }
1286
+ /** The builder's own patterns for one detector; `field` is the draft field their issues are reported at. */
1287
+ function PatternList({ detector, field, patterns, onChange, }) {
1288
+ const status = useFieldStatus()(field);
1289
+ return (_jsxs(_Fragment, { children: [patterns.map((pattern, index) => (_jsx(PatternRows
1290
+ // biome-ignore lint/suspicious/noArrayIndexKey: patterns have no id and are only appended or removed
1291
+ , { detector: detector, pattern: pattern, index: index, status: status, onChange: (next) => {
1292
+ onChange(patterns.map((each, at) => (at === index ? next : each)));
1293
+ }, onRemove: () => {
1294
+ onChange(patterns.filter((_, at) => at !== index));
1295
+ } }, index))), _jsx(Button, { label: "Add pattern", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), onClick: () => {
1296
+ onChange([...patterns, newPattern()]);
1297
+ } })] }));
1298
+ }
1299
+ const PATTERN_USE_SEGMENTS = [
1300
+ {
1301
+ value: 'theorem',
1302
+ label: "theorem's",
1303
+ icon: IconTheorem,
1304
+ description: sectionNote('detect.source.theorem'),
1305
+ },
1306
+ {
1307
+ value: 'mine',
1308
+ label: 'Mine',
1309
+ icon: IconWriting,
1310
+ description: sectionNote('detect.source.mine'),
1311
+ },
1312
+ {
1313
+ value: 'both',
1314
+ label: 'Both',
1315
+ icon: IconLayersUnion,
1316
+ description: sectionNote('detect.source.both'),
1317
+ },
1318
+ ];
1319
+ /**
1320
+ * Whose patterns one of Theorem's detectors reads with, and the builder's when it reads with them.
1321
+ * Patterns set aside by a move to Theorem's alone come back on a move away, while the page is open.
1322
+ */
1323
+ function PatternSourceSection({ detector, source, onChange, }) {
1324
+ const setAside = useRef([]);
1325
+ const mine = source.patterns.length > 0;
1326
+ const use = source.theorem ? (mine ? 'both' : 'theorem') : 'mine';
1327
+ return (_jsxs(InspectorSection, { title: "Patterns", children: [_jsx(SegmentedRow, { label: "Read with", path: `guardrails.detect.${detector}.theorem`, value: use, segments: PATTERN_USE_SEGMENTS, onChange: (next) => {
1328
+ if (next === 'theorem') {
1329
+ setAside.current = source.patterns;
1330
+ onChange({ ...source, theorem: true, patterns: [] });
1331
+ return;
1332
+ }
1333
+ const kept = setAside.current.length ? setAside.current : [newPattern()];
1334
+ onChange({
1335
+ ...source,
1336
+ theorem: next === 'both',
1337
+ patterns: mine ? source.patterns : kept,
1338
+ });
1339
+ } }), use !== 'theorem' && (_jsx(PatternList, { detector: detector, field: `sources.${detector}.patterns`, patterns: source.patterns, onChange: (patterns) => {
1340
+ onChange({ ...source, patterns });
1341
+ } })), mine && (_jsx(TextRow, { label: "Retry hint", path: `guardrails.detect.${detector}.hint`, field: `sources.${detector}.hint`, value: source.hint, onChange: (hint) => {
1342
+ onChange({ ...source, hint });
1343
+ } }))] }));
1344
+ }
1345
+ function isUrlDetector(detector) {
1346
+ return DETECTOR_META[detector].allow === 'urls';
1347
+ }
1348
+ /** A problem with the detectors as a whole, such as a key of the builder's the kernel refuses. */
1349
+ function DetectIssue() {
1350
+ const status = useFieldStatus()('detect');
1351
+ return status?.message ? _jsx(Banner, { status: "error", title: status.message }) : null;
1352
+ }
1353
+ /**
1354
+ * What a detector's row says when a field on its page has an issue, so the list shows where: the
1355
+ * issue when there is one, a count when there are more, since the page says each on its own row.
1356
+ */
1357
+ function usePageIssues() {
1358
+ const issues = useContext(NodeIssues);
1359
+ return (onPage) => {
1360
+ const messages = issues.filter(onPage).map((issue) => issue.message);
1361
+ if (messages.length === 0)
1362
+ return undefined;
1363
+ const [only] = messages;
1364
+ return {
1365
+ type: 'error',
1366
+ message: messages.length === 1 ? only : `${String(messages.length)} things to fix`,
1367
+ };
1368
+ };
1369
+ }
1370
+ /** The button at the end of a detector's row that opens its page. */
1371
+ function OpenDetectorButton({ label, onOpen }) {
1372
+ const open = `Open ${label}: patterns and boundaries`;
1373
+ return (_jsx(Tooltip, { content: open, children: _jsx(IconButton, { label: open, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconChevronRight, size: "sm" }), onClick: onOpen }) }));
1374
+ }
1375
+ /** What an own detector is called before it has a label. */
1376
+ const ownName = (detector) => detector.label.trim() || detector.key.trim() || 'New detector';
1377
+ /**
1378
+ * Every detector on one line: its name and what it does with a match, then the button that opens
1379
+ * its page. Theorem's sit under their group, the builder's own under "Custom detectors", and one that applies
1380
+ * at no boundary the profile has is left out.
1381
+ */
1382
+ function DetectSection({ guardrails, boundaries, set, onOpen, }) {
1383
+ const { detect, own } = guardrails;
1384
+ const pageIssue = usePageIssues();
1385
+ const has = new Set(boundaries);
1386
+ const groups = DETECTOR_GROUPS.map((group) => ({
1387
+ group,
1388
+ rows: DETECTORS.filter((detector) => DETECTOR_META[detector].group === group)
1389
+ .map((detector) => ({
1390
+ detector,
1391
+ applies: DETECTOR_BOUNDARIES[detector].filter((boundary) => has.has(boundary)),
1392
+ }))
1393
+ .filter(({ applies }) => applies.length > 0),
1394
+ })).filter(({ rows }) => rows.length > 0);
1395
+ return (_jsxs(InspectorSection, { title: "Detect", path: "guardrails.detect", children: [_jsx(DetectIssue, {}), groups.map(({ group, rows }) => (_jsxs(VStack, { gap: 2, children: [_jsx(InspectorGroupTitle, { title: DETECTOR_GROUP_META[group].label, hint: DETECTOR_GROUP_META[group].doc }), rows.map(({ detector, applies }) => (_jsx(ActionRow, { detector: detector, label: DETECTOR_META[detector].short ?? DETECTOR_META[detector].label, defaults: DETECTOR_META[detector].defaults, boundaries: applies, actions: detect[detector], status: pageIssue(({ field }) => field?.startsWith(`sources.${detector}.`) === true ||
1396
+ field?.startsWith(`allow.${detector}.`) === true ||
1397
+ (field === 'innocentNames' && DETECTOR_META[detector].allow === 'names')), trailing: _jsx(OpenDetectorButton, { label: DETECTOR_META[detector].label, onOpen: () => {
1398
+ onOpen({ detector });
1399
+ } }), onChange: (actions) => {
1400
+ set({ detect: { ...detect, [detector]: actions } });
1401
+ } }, detector)))] }, group))), _jsxs(VStack, { gap: 2, children: [_jsx(InspectorGroupTitle, { title: "Custom detectors", hint: sectionNote('detect.own') }), own.map((detector, index) => (_jsx(ActionRow
1402
+ // biome-ignore lint/suspicious/noArrayIndexKey: detectors are keyed by text the builder is typing
1403
+ , { detector: "*", label: ownName(detector), boundaries: boundaries, actions: detector.at, status: pageIssue(({ field, index: at }) => (field === 'own' && at === index) ||
1404
+ field?.startsWith(`own.${String(index)}.`) === true), trailing: _jsx(OpenDetectorButton, { label: ownName(detector), onOpen: () => {
1405
+ onOpen({ own: index });
1406
+ } }), onChange: (at) => {
1407
+ set({ own: own.map((each, i) => (i === index ? { ...detector, at } : each)) });
1408
+ } }, index))), _jsx(Button, { label: "Add detector", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), onClick: () => {
1409
+ set({ own: [...own, newOwnDetector()] });
1410
+ onOpen({ own: own.length });
1411
+ } })] })] }));
1412
+ }
1413
+ /**
1414
+ * The top of a page inside a node, such as a detector's: the way back to the list, its name, and
1415
+ * what it is, with anything wrong with the list as a whole under them.
1416
+ */
1417
+ function PageHeader({ title, doc, back, trailing, onBack, children, }) {
1418
+ return (_jsx(Section, { variant: "transparent", padding: 3, children: _jsxs(VStack, { gap: 1, children: [_jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(Tooltip, { content: back, children: _jsx(IconButton, { label: back, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconArrowLeft, size: "sm" }), onClick: onBack }) }), _jsx(StackItem, { size: "fill", children: _jsx(Text, { type: "label", weight: "semibold", children: title }) }), trailing] }), doc && _jsx(Text, { type: "supporting", children: doc }), children] }) }));
1419
+ }
1420
+ /** One of Theorem's detectors: its action, whose patterns it reads with, what it lets through, its boundaries. */
1421
+ function DetectorPage({ detector, boundaries, hasCanary, guardrails, set, onBack, }) {
1422
+ const { detect, allow, sources, innocentNames } = guardrails;
1423
+ const meta = DETECTOR_META[detector];
1424
+ const setActions = (actions) => {
1425
+ set({ detect: { ...detect, [detector]: actions } });
1426
+ };
1427
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: meta.label, doc: meta.doc, back: "Back to detectors", onBack: onBack, children: _jsx(DetectIssue, {}) }), _jsx(ActionSection, { detector: detector, defaults: meta.defaults, boundaries: boundaries, actions: detect[detector], onChange: setActions }), detector === 'canary_leak' && hasCanary && (_jsx(CanarySection, { guardrails: guardrails, set: set })), meta.patterns && (_jsx(PatternSourceSection, { detector: detector, source: sources[detector], onChange: (next) => {
1428
+ set({ sources: { ...sources, [detector]: next } });
1429
+ } })), isUrlDetector(detector) && (_jsx(InspectorSection, { title: "Allow", children: _jsx(UrlAllowRows, { detector: detector, allow: allow[detector], onChange: (next) => {
1430
+ set({ allow: { ...allow, [detector]: next } });
1431
+ } }) })), meta.allow === 'names' && (_jsx(InspectorSection, { title: "Allow", children: _jsx(NamesRow, { label: "Names", path: `guardrails.detect.${detector}.allow.names`, field: "innocentNames", value: innocentNames, onChange: (names) => {
1432
+ set({ innocentNames: names });
1433
+ } }) })), _jsx(Suspense, { fallback: null, children: _jsx(DetectorTester, { only: detector }) })] }));
1434
+ }
1435
+ /** One detector of the builder's own: its name and key, its action, its patterns, its boundaries. */
1436
+ function OwnDetectorPage({ detector, index, boundaries, onChange, onRemove, onBack, }) {
1437
+ const statusAt = useFieldStatus();
1438
+ const status = statusAt('own', index);
1439
+ const remove = `Remove ${ownName(detector)}`;
1440
+ const setActions = (at) => {
1441
+ onChange({ ...detector, at });
1442
+ };
1443
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: ownName(detector), doc: fieldMeta('guardrails.detect.*')?.doc, back: "Back to detectors", trailing: _jsx(Tooltip, { content: remove, children: _jsx(IconButton, { label: remove, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconTrash, size: "sm" }), onClick: onRemove }) }), onBack: onBack, children: _jsx(DetectIssue, {}) }), _jsxs(InspectorSection, { title: "Detector", children: [_jsx(TextRow, { label: "Name", path: "guardrails.detect.*.label", field: `own.${String(index)}.label`, value: detector.label, placeholder: "Codenames", onChange: (label) => {
1444
+ onChange({ ...detector, label });
1445
+ } }), _jsx(TextRow, { label: "Key", path: "guardrails.detect.*", status: status, value: detector.key, placeholder: "acme.codenames", onChange: (key) => {
1446
+ onChange({ ...detector, key });
1447
+ } })] }), _jsx(ActionSection, { detector: "*", boundaries: boundaries, actions: detector.at, status: statusAt(`own.${String(index)}.at`), onChange: setActions }), _jsxs(InspectorSection, { children: [_jsx(PatternList, { detector: "*", field: `own.${String(index)}.patterns`, patterns: detector.patterns, onChange: (patterns) => {
1448
+ onChange({ ...detector, patterns });
1449
+ } }), _jsx(TextRow, { label: "Retry hint", path: "guardrails.detect.*.hint", field: `own.${String(index)}.hint`, value: detector.hint, onChange: (hint) => {
1450
+ onChange({ ...detector, hint });
1451
+ } })] }), _jsx(Suspense, { fallback: null, children: _jsx(DetectorTester, { only: detector.key }) })] }));
1452
+ }
1453
+ /** The page for `open`, or `undefined` when what it names is gone or applies nowhere in this profile. */
1454
+ function openDetectorPage(open, { guardrails, set }, boundaries, hasCanary, onBack) {
1455
+ if (open === null)
1456
+ return undefined;
1457
+ if ('detector' in open) {
1458
+ const has = new Set(boundaries);
1459
+ const applies = DETECTOR_BOUNDARIES[open.detector].filter((boundary) => has.has(boundary));
1460
+ if (applies.length === 0)
1461
+ return undefined;
1462
+ return (_jsx(DetectorPage, { detector: open.detector, boundaries: applies, hasCanary: hasCanary, guardrails: guardrails, set: set, onBack: onBack }));
1463
+ }
1464
+ const { own } = guardrails;
1465
+ const detector = own.at(open.own);
1466
+ if (!detector)
1467
+ return undefined;
1468
+ return (_jsx(OwnDetectorPage, { detector: detector, index: open.own, boundaries: boundaries, onChange: (next) => {
1469
+ set({ own: own.map((each, at) => (at === open.own ? next : each)) });
1470
+ }, onRemove: () => {
1471
+ set({ own: own.filter((_, at) => at !== open.own) });
1472
+ onBack();
1473
+ }, onBack: onBack }));
1474
+ }
1475
+ /** The nearest element above `node` that scrolls it. */
1476
+ function scrollerOf(node) {
1477
+ for (let up = node.parentElement; up; up = up.parentElement) {
1478
+ if (/auto|scroll/.test(getComputedStyle(up).overflowY))
1479
+ return up;
1480
+ }
1481
+ return null;
1482
+ }
1483
+ /** Fades `node` out at the theme's fast duration and standard curve, at once when motion is off. */
1484
+ async function fadeOut(node) {
1485
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches)
1486
+ return;
1487
+ const style = getComputedStyle(node);
1488
+ const time = style.getPropertyValue('--duration-fast').trim();
1489
+ const fade = node.animate([{ opacity: 1 }, { opacity: 0 }], {
1490
+ duration: Number.parseFloat(time) * (time.endsWith('ms') ? 1 : 1000),
1491
+ easing: style.getPropertyValue('--ease-standard').trim(),
1492
+ fill: 'forwards',
1493
+ });
1494
+ await fade.finished.catch(() => undefined);
1495
+ }
1496
+ /**
1497
+ * A node's list and the page one of its rows opens, one at a time: `show` fades the view out and
1498
+ * swaps it, and `frame` holds what is shown. A page opens at its top, and the list comes back
1499
+ * where it was left.
1500
+ */
1501
+ function usePageSwap() {
1502
+ const [open, setOpen] = useState(null);
1503
+ const view = useRef(null);
1504
+ // Where the list was scrolled to when a page opened, to put it back there.
1505
+ const listScroll = useRef(0);
1506
+ const leavePage = useContext(LeavePage);
1507
+ const show = (next) => {
1508
+ const node = view.current;
1509
+ if (node && open === null)
1510
+ listScroll.current = scrollerOf(node)?.scrollTop ?? 0;
1511
+ const left = leavePage();
1512
+ const swap = () => {
1513
+ left();
1514
+ setOpen(next);
1515
+ };
1516
+ if (node)
1517
+ void fadeOut(node).then(swap);
1518
+ else
1519
+ swap();
1520
+ };
1521
+ /** `viewKey` is `'list'` for the list, and names the page otherwise. */
1522
+ const frame = (viewKey, children) => (_jsx(PageFrame, { viewKey: viewKey, view: view, listScroll: listScroll, children: children }));
1523
+ return { open, show, frame };
1524
+ }
1525
+ /** What `usePageSwap` shows; the view that mounts fades in (`.editor-page`). */
1526
+ function PageFrame({ viewKey, view, listScroll, children, }) {
1527
+ const shown = useRef(viewKey);
1528
+ // Before either fades in.
1529
+ useLayoutEffect(() => {
1530
+ const node = view.current;
1531
+ if (!node || shown.current === viewKey)
1532
+ return;
1533
+ shown.current = viewKey;
1534
+ if (viewKey === 'list')
1535
+ scrollerOf(node)?.scrollTo({ top: listScroll.current });
1536
+ else
1537
+ node.scrollIntoView({ block: 'start' });
1538
+ }, [viewKey, view, listScroll]);
1539
+ return (_jsx("div", { ref: view, className: "editor-page", children: children }, viewKey));
1540
+ }
1541
+ function GuardrailsEditor({ draft, setDraft }) {
1542
+ const { guardrails } = draft;
1543
+ const set = patch(setDraft, 'guardrails');
1544
+ const on = useContext(RunsOn);
1545
+ const boundaries = draft.identity.profileType === 'host' ? TOOL_BOUNDARIES : BOUNDARIES;
1546
+ const { open, show, frame } = usePageSwap();
1547
+ const page = openDetectorPage(open, { guardrails, set }, boundaries, plantsCanary(draft), () => {
1548
+ show(null);
1549
+ });
1550
+ if (page)
1551
+ return frame(JSON.stringify(open), page);
1552
+ return frame('list', _jsxs(_Fragment, { children: [_jsx(DetectSection, { guardrails: guardrails, boundaries: boundaries, set: set, onOpen: show }), draftAllows(draft, 'guardrails.blockedReply') && (_jsx(BlockedReplySection, { guardrails: guardrails, withheld: labelValue(draft, 'error.safety') || labelDefault('error.safety'), set: set })), _jsx(NetworkSection, { guardrails: guardrails, set: set }), draftAllows(draft, 'guardrails.taint') && (_jsxs(InspectorSection, { title: "After a remote read", path: "guardrails.taint", note: studioTaintNote(on), children: [_jsx(SegmentedRow, { label: "Refuse calls", path: "guardrails.taint.afterRemoteRead", value: guardrails.taintAfterRemoteRead || 'off', segments: TAINT_SEGMENTS, onChange: (gate) => {
1553
+ set({ taintAfterRemoteRead: gate === 'off' ? '' : gate });
1554
+ } }), _jsx(SegmentedRow, { label: "New address", path: "guardrails.taint.remoteDestination", value: guardrails.taintRemoteDestination || 'off', segments: DESTINATION_SEGMENTS, onChange: (gate) => {
1555
+ set({ taintRemoteDestination: gate === 'off' ? '' : gate });
1556
+ } })] })), _jsx(QuotaSection, { guardrails: guardrails, set: set })] }));
1557
+ }
1558
+ /** Traces go to the studio's store, or to the store a project's profile names, shown by its name. */
1559
+ function traceSegments(destination) {
1560
+ const on = destination === STUDIO_TRACE_DESTINATION
1561
+ ? { label: 'Studio', icon: IconFlask }
1562
+ : { label: destination, icon: IconDatabase };
1563
+ return [
1564
+ { value: 'studio', ...on },
1565
+ { value: 'off', label: 'Off', icon: IconEyeOff },
1566
+ ];
1567
+ }
1568
+ const INCLUDE_FLAGS = [
1569
+ { key: 'upstreamLog', label: 'Upstream log' },
1570
+ { key: 'outboundWire', label: 'Outbound wire' },
1571
+ { key: 'evidenceRaw', label: 'Raw evidence' },
1572
+ { key: 'usage', label: 'Usage' },
1573
+ { key: 'guardrailDecisions', label: 'Decisions' },
1574
+ { key: 'guardrailMatchPreview', label: 'Match preview' },
1575
+ ];
1576
+ const SCRUB_FLAGS = [
1577
+ { key: 'sensitive', label: 'Sensitive' },
1578
+ { key: 'injection', label: 'Injection' },
1579
+ { key: 'canary', label: 'Canary' },
1580
+ ];
1581
+ /** A set of on-or-off flags as one checklist: the ones on are checked. */
1582
+ function FlagList({ label, path, flags, value, onChange, }) {
1583
+ return (_jsx(CheckboxList, { label: label, isLabelHidden: true, density: "compact", value: flags.filter((flag) => value[flag.key]).map((flag) => flag.key), onChange: (checked) => {
1584
+ const next = { ...value };
1585
+ for (const flag of flags)
1586
+ next[flag.key] = checked.includes(flag.key);
1587
+ onChange(next);
1588
+ }, children: flags.map((flag) => (_jsx(CheckboxListItem, { value: flag.key, label: flag.label,
1589
+ // A node, not a string, so the kernel's whole entry wraps instead of truncating.
1590
+ description: _jsx(Text, { type: "supporting", children: fieldMeta(`${path}.${flag.key}`)?.doc }) }, flag.key))) }));
1591
+ }
1592
+ const PERCENT = new Intl.NumberFormat('en-US', { style: 'percent' });
1593
+ const RESOURCE_PLACEHOLDER = `{
1594
+ "service.name": "my-agent"
1595
+ }`;
1596
+ /** Whether traces are written, and how many turns are sampled. */
1597
+ function TracesSection({ observability, set }) {
1598
+ const on = observability.writeTo !== false;
1599
+ return (_jsxs(InspectorSection, { title: "Traces", path: "observability", children: [_jsx(SegmentedRow, { label: "Write to", path: "observability.writeTo", value: on ? 'studio' : 'off', segments: traceSegments(observability.destination), onChange: (segment) => {
1600
+ set({ writeTo: segment === 'off' ? false : observability.destination });
1601
+ } }), on && (_jsx(SliderRow, { label: "Sample", path: "observability.sampleRate", field: "sampleRate", value: observability.sampleRate, min: 0, max: 1, step: 0.05, format: (rate) => PERCENT.format(rate), onChange: (sampleRate) => {
1602
+ set({ sampleRate });
1603
+ } }))] }));
1604
+ }
1605
+ /** What a trace keeps, and what is scrubbed from it. */
1606
+ function TraceContentSections({ observability, set }) {
1607
+ return (_jsxs(_Fragment, { children: [_jsx(InspectorSection, { path: "observability.include", children: _jsx(FlagList, { label: "Keep", path: "observability.include", flags: INCLUDE_FLAGS, value: observability.include, onChange: (include) => {
1608
+ set({ include });
1609
+ } }) }), _jsx(InspectorSection, { path: "observability.scrub", children: _jsx(FlagList, { label: "Scrub", path: "observability.scrub", flags: SCRUB_FLAGS, value: observability.scrub, onChange: (scrub) => {
1610
+ set({ scrub });
1611
+ } }) })] }));
1612
+ }
1613
+ function TraceStorageSection({ observability, set }) {
1614
+ return (_jsxs(InspectorSection, { title: "Storage", note: sectionNote('traces.storage'), children: [_jsx(NumberRow, { label: "Keep for", path: "observability.retainForDays", field: "retainForDays", units: "days", value: observability.retainForDays, isIntegerOnly: true, onChange: (retainForDays) => {
1615
+ set({ retainForDays });
1616
+ } }), _jsx(NumberRow, { label: "Rotate at", path: "observability.rotateAfterMiB", field: "rotateAfterMiB", units: "MiB", value: observability.rotateAfterMiB, min: 1, onChange: (rotateAfterMiB) => {
1617
+ set({ rotateAfterMiB });
1618
+ } }), _jsx(TextAreaRow, { label: "Resource", path: "observability.resource", field: "resourceJson", value: observability.resourceJson, rows: 3, hasSpellCheck: false, placeholder: RESOURCE_PLACEHOLDER, onChange: (resourceJson) => {
1619
+ set({ resourceJson });
1620
+ } })] }));
1621
+ }
1622
+ /** Where traces go, what they keep, and how your host stores them. */
1623
+ function ObservabilityEditor({ draft, setDraft }) {
1624
+ const { observability } = draft;
1625
+ const set = patch(setDraft, 'observability');
1626
+ const on = observability.writeTo !== false;
1627
+ return (_jsxs(_Fragment, { children: [_jsx(TracesSection, { observability: observability, set: set }), on && (_jsxs(_Fragment, { children: [_jsx(TraceContentSections, { observability: observability, set: set }), _jsx(TraceStorageSection, { observability: observability, set: set })] }))] }));
1628
+ }
1629
+ const TOOL_TYPE_SEGMENTS = [
1630
+ { value: 'function', label: 'Function', icon: TOOL_TYPE_ICON.function },
1631
+ { value: 'http', label: 'HTTP', icon: TOOL_TYPE_ICON.http },
1632
+ { value: 'mcp', label: 'MCP', icon: TOOL_TYPE_ICON.mcp },
1633
+ { value: 'agent', label: 'Agent', icon: TOOL_TYPE_ICON.agent },
1634
+ ];
1635
+ /** The agents an agent tool can run. */
1636
+ const CALLABLE_TYPES = new Set(['text', 'image', 'speech']);
1637
+ const ACCESS_SEGMENTS = [
1638
+ { value: 'read-only', label: 'Read only', icon: IconEye },
1639
+ { value: 'read-write', label: 'Read and write', icon: IconPencil },
1640
+ { value: 'destructive', label: 'Destructive', icon: IconFlame },
1641
+ ];
1642
+ const PERMISSION_SEGMENTS = [
1643
+ { value: 'auto', label: 'Runs', icon: IconPlayerPlay },
1644
+ { value: 'session_consent', label: 'Once a session', icon: IconUserCheck },
1645
+ { value: 'always_confirm', label: 'Every call', icon: IconHandStop },
1646
+ ];
1647
+ const METHOD_SEGMENTS = [
1648
+ { value: 'GET', label: 'GET', icon: IconHttpGet },
1649
+ { value: 'POST', label: 'POST', icon: IconHttpPost },
1650
+ { value: 'PUT', label: 'PUT', icon: IconHttpPut },
1651
+ { value: 'PATCH', label: 'PATCH', icon: IconHttpPatch },
1652
+ { value: 'DELETE', label: 'DELETE', icon: IconHttpDelete },
1653
+ ];
1654
+ const LOAD_TIER_SEGMENTS = [
1655
+ { value: 'T0', label: 'T0', icon: IconSquareRoundedNumber0 },
1656
+ { value: 'T2', label: 'T2', icon: IconSquareRoundedNumber2 },
1657
+ ];
1658
+ const AUTH_TYPE_SEGMENTS = [
1659
+ { value: 'none', label: 'None', icon: IconLockOpen },
1660
+ { value: 'bearer', label: 'Bearer', icon: IconCertificate },
1661
+ { value: 'api_key', label: 'API key', icon: IconKey },
1662
+ { value: 'oauth2', label: 'OAuth 2', icon: IconShieldLock },
1663
+ ];
1664
+ const UNAUTHENTICATED_SEGMENTS = [
1665
+ { value: 'gate', label: 'Stop the turn', icon: IconPlayerPause },
1666
+ { value: 'report_to_model', label: 'Tell the model', icon: IconMessage },
1667
+ { value: 'fail', label: 'Fail the call', icon: IconX },
1668
+ ];
1669
+ /** The prefix the kernel puts before the credential when none is set, by auth type. */
1670
+ const AUTH_HEADER_PREFIX = {
1671
+ bearer: 'Bearer ',
1672
+ api_key: '',
1673
+ oauth2: 'Bearer ',
1674
+ };
1675
+ /**
1676
+ * Why a tool on `tier` never loads on this draft: the studio can't write a T1 policy, so a
1677
+ * T2 tool loads only through the T2 loader.
1678
+ */
1679
+ function loadTierWarning(draft, tier) {
1680
+ if (tier === 'T2' && draftAllows(draft, 'tools.t2Loader') && !draft.tools.t2Loader.trim()) {
1681
+ return 'No T2 loader is set under Tools, so this tool never loads.';
1682
+ }
1683
+ return undefined;
1684
+ }
1685
+ function placeholderHint(schemas) {
1686
+ const names = new Set();
1687
+ for (const json of schemas) {
1688
+ try {
1689
+ const schema = JSON.parse(json);
1690
+ const properties = schema && typeof schema === 'object' && 'properties' in schema
1691
+ ? schema.properties
1692
+ : undefined;
1693
+ if (properties && typeof properties === 'object') {
1694
+ for (const name of Object.keys(properties))
1695
+ names.add(`{${name}}`);
1696
+ }
1697
+ }
1698
+ catch {
1699
+ // A schema mid-edit names nothing yet.
1700
+ }
1701
+ }
1702
+ return names.size ? `Can use ${[...names].join(' ')}` : undefined;
1703
+ }
1704
+ const HEADERS_PLACEHOLDER = `{
1705
+ "Accept": "application/json"
1706
+ }`;
1707
+ /** Adds a new tool to the draft and opens it. */
1708
+ export function addToolSpec(draft, setDraft, onSelect) {
1709
+ const tool = newToolSpec(draft);
1710
+ setDraft((current) => ({ ...current, toolSpecs: [...current.toolSpecs, tool] }));
1711
+ onSelect(toolSpecNodeId(tool.key));
1712
+ }
1713
+ /** The library's tools as a checklist, the ones this agent may use checked. A row's pencil opens the tool. */
1714
+ function AllowedToolsList({ tools, allowed, workspace, onSelect, }) {
1715
+ return (_jsx(CheckboxList, { label: "Allowed", isLabelHidden: true, density: "compact", value: allowed.map((tool) => tool.key), onChange: (checked) => {
1716
+ for (const tool of tools) {
1717
+ workspace.setAllowed(tool.key, checked.includes(tool.key));
1718
+ }
1719
+ }, children: tools.map((tool) => {
1720
+ // A tool that runs this agent can't be turned on here; one already on can still be turned off.
1721
+ const runsSelf = tool.toolType === 'agent' &&
1722
+ tool.agentKey === workspace.self &&
1723
+ !workspace.allowed.includes(tool.key);
1724
+ const name = tool.toolName.trim() || 'Unnamed tool';
1725
+ const description = runsSelf
1726
+ ? 'Runs this agent, so this agent can’t use it.'
1727
+ : tool.description.trim();
1728
+ return (_jsx(CheckboxListItem, { value: tool.key, label: name, isDisabled: runsSelf, endContent: _jsx("span", { className: "row-reveal", children: _jsx(Tooltip, { content: `Edit ${name}`, children: _jsx(IconButton, { label: `Edit ${name}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPencil, size: "sm" }), onClick: () => {
1729
+ onSelect(toolSpecNodeId(tool.key));
1730
+ } }) }) }), description: description ? _jsx(Text, { type: "supporting", children: description }) : undefined }, tool.key));
1731
+ }) }));
1732
+ }
1733
+ /** An agent's own tools, each opening its editor. */
1734
+ function ToolList({ tools, onSelect }) {
1735
+ return (_jsx(List, { density: "compact", children: tools.map((tool) => (_jsx(ListItem, { label: tool.toolName.trim() || 'Unnamed tool', description: tool.description.trim() || undefined, startContent: _jsx(Icon, { icon: toolTypeIcon(tool.toolType), size: "sm", color: "secondary" }), endContent: _jsx(Token, { label: tool.loadTier, size: "sm" }), onClick: () => {
1736
+ onSelect(toolSpecNodeId(tool.key));
1737
+ } }, tool.key))) }));
1738
+ }
1739
+ /** The tools, each opening its own editor, and the T2 loader. */
1740
+ function ToolsEditor({ draft, setDraft, onSelect, }) {
1741
+ const set = patch(setDraft, 'tools');
1742
+ const workspace = useContext(WorkspaceContext);
1743
+ const isHost = draft.identity.profileType === 'host';
1744
+ /** In a workspace, the library's tools this agent allows; on its own, every tool it has. */
1745
+ const allowed = workspace
1746
+ ? draft.toolSpecs.filter((tool) => workspace.allowed.includes(tool.key))
1747
+ : draft.toolSpecs;
1748
+ const unused = workspace
1749
+ ? draft.toolSpecs.filter((tool) => !workspace.allowed.includes(tool.key))
1750
+ : [];
1751
+ // Any custom tool it allows can load T2 tools: one that answers with the ids to load.
1752
+ const loaders = allowed.map((tool) => tool.toolName.trim()).filter(Boolean);
1753
+ const note = isHost ? sectionNote('tools.host') : undefined;
1754
+ const addTool = (_jsx(Button, { label: "Add tool", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), onClick: () => {
1755
+ addToolSpec(draft, setDraft, onSelect);
1756
+ } }));
1757
+ return (_jsxs(_Fragment, { children: [workspace ? (_jsxs(InspectorSection, { path: "tools.allow", note: note, children: [allowed.length > 0 && (_jsx(AllowedToolsList, { tools: allowed, allowed: allowed, workspace: workspace, onSelect: onSelect })), unused.length > 0 && (_jsx(CollapsibleGroup, { type: "multiple", density: "compact", children: _jsx(Collapsible, { value: "library", trigger: _jsxs(Text, { type: "supporting", children: ["Library, not used by this agent (", String(unused.length), ")"] }), children: _jsx(AllowedToolsList, { tools: unused, allowed: [], workspace: workspace, onSelect: onSelect }) }) })), addTool] })) : (_jsxs(InspectorSection, { path: isHost ? undefined : 'tools', note: note, children: [draft.toolSpecs.length > 0 && _jsx(ToolList, { tools: draft.toolSpecs, onSelect: onSelect }), addTool] })), draftAllows(draft, 'tools.t2Loader') && (_jsx(ToolLoadingSection, { value: draft.tools.t2Loader, loaders: loaders, set: set })), draftAllows(draft, 'tools.parallel') && (_jsx(InspectorSection, { path: "tools.parallel", children: _jsx(SwitchRow, { label: "Parallel", path: "tools.parallel", field: "parallel", value: draft.tools.parallel, onChange: (parallel) => {
1758
+ set({ parallel });
1759
+ } }) }))] }));
1760
+ }
1761
+ /** The tool that loads T2 tools, picked from the custom tools the agent allows. */
1762
+ function ToolLoadingSection({ value, loaders, set, }) {
1763
+ return (_jsx(InspectorSection, { path: "tools.t2Loader", children: _jsx(ChoiceRow, { label: "T2 loader", path: "tools.t2Loader", field: "t2Loader", value: value, options: loaders, onChange: (t2Loader) => {
1764
+ set({ t2Loader });
1765
+ } }) }));
1766
+ }
1767
+ const MS = new Intl.NumberFormat('en-US', { style: 'unit', unit: 'millisecond' });
1768
+ /** The result's headline: the status line, or for an MCP server, what it offers. */
1769
+ function probeTitle(result, tool) {
1770
+ const took = result.elapsedMs === undefined ? '' : ` · ${MS.format(result.elapsedMs)}`;
1771
+ if (!result.ok && result.error)
1772
+ return result.error;
1773
+ if (tool.toolType === 'mcp' && result.tools) {
1774
+ // The route only checks for the remote tool when one is named.
1775
+ const name = tool.mcpToolName?.trim() ?? '';
1776
+ if (result.targetToolFound === false)
1777
+ return `Connected, but the server has no ${name}${took}`;
1778
+ if (result.targetToolFound)
1779
+ return `Connected, and ${name} is there${took}`;
1780
+ return `Connected: ${String(result.tools.length)} tools${took}`;
1781
+ }
1782
+ return `${String(result.status ?? '')} ${result.statusText ?? ''}`.trim() + took;
1783
+ }
1784
+ /** The sample input for the tool, which follows its input schema, as JSON; blank without one. */
1785
+ function defaultSampleInput(tool) {
1786
+ const sample = sampleToolInput(tool.toolName, tool.inputJson);
1787
+ return sample ? JSON.stringify(sample, null, 2) : '';
1788
+ }
1789
+ /** What a test reaches and sends, as one comparable string. */
1790
+ function probeTarget(tool, method) {
1791
+ return JSON.stringify([
1792
+ tool.toolType,
1793
+ tool.endpoint,
1794
+ method,
1795
+ tool.serverUrl,
1796
+ tool.mcpToolName,
1797
+ tool.headersJson,
1798
+ tool.authType,
1799
+ tool.authHeaderName,
1800
+ tool.authHeaderPrefix,
1801
+ tool.authQueryParam,
1802
+ ]);
1803
+ }
1804
+ /** The result's banner status: a missing remote tool warns even though the server answered. */
1805
+ function probeStatus(result) {
1806
+ if (result.ok && result.targetToolFound === false)
1807
+ return 'warning';
1808
+ return result.ok ? 'success' : 'error';
1809
+ }
1810
+ /** The result's detail: an MCP server's tools, or the start of the response. */
1811
+ function probeDetails(result) {
1812
+ if (result.tools?.length)
1813
+ return (_jsx(HStack, { gap: 1, wrap: "wrap", children: result.tools.map((name) => (_jsx(Token, { label: name, size: "sm" }, name))) }));
1814
+ if (!result.preview)
1815
+ return undefined;
1816
+ return (_jsx(CodeBlock, { code: result.preview, language: result.preview.trimStart().startsWith('{') ? 'json' : 'text', hasLanguageLabel: false, isWrapped: true, size: "sm", maxHeight: 200 }));
1817
+ }
1818
+ /** The test's credential, typed for this test only. */
1819
+ function TestCredentialRow({ value, onChange, }) {
1820
+ return (_jsx(InspectorRow, { label: "Credential", path: "studio.testCredential", children: _jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: "Credential", isLabelHidden: true, size: "sm", type: "password", autoComplete: "off", value: value, placeholder: "Used once, never saved", onChange: onChange }) }) }));
1821
+ }
1822
+ /** The input an HTTP tool's test sends. */
1823
+ function SampleInputRow({ value, onChange }) {
1824
+ return (_jsx(TextAreaRow, { label: "Sample input", path: "studio.sampleInput", value: value, rows: 4, hasSpellCheck: false, placeholder: "{}", onChange: onChange }));
1825
+ }
1826
+ function TestConnectionButton({ isLoading, onClick }) {
1827
+ return (_jsx(HStack, { children: _jsx(Button, { label: "Test connection", variant: "secondary", size: "sm", icon: _jsx(Icon, { icon: IconPlugConnected, size: "sm" }), isLoading: isLoading, onClick: onClick }) }));
1828
+ }
1829
+ /**
1830
+ * Tries the tool's endpoint or MCP server once, from the server, which reaches public hosts only.
1831
+ * The sample input and credential live here only: neither is saved to the draft.
1832
+ */
1833
+ function ToolTest({ tool, guardrails }) {
1834
+ /** What the user typed; until then, the sample for the tool, which follows its input schema. */
1835
+ const [typedInput, setTypedInput] = useState();
1836
+ const sampleInput = typedInput ?? defaultSampleInput(tool);
1837
+ const credential = useSyncExternalStore(subscribeToolCredentials, () => toolCredential(tool.key), () => '');
1838
+ const setCredential = (value) => {
1839
+ setToolCredential(tool.key, value);
1840
+ };
1841
+ const [pending, setPending] = useState(false);
1842
+ const [result, setResult] = useState();
1843
+ const http = tool.toolType === 'http';
1844
+ const method = tool.method ?? HTTP_METHODS[0];
1845
+ // A result answers the request as it was: once what it reaches or sends changes, it's dropped.
1846
+ const target = probeTarget(tool, method);
1847
+ const [testedTarget, setTestedTarget] = useState(target);
1848
+ if (target !== testedTarget) {
1849
+ setTestedTarget(target);
1850
+ setResult(undefined);
1851
+ }
1852
+ const confirmWrite = useContext(ConfirmWrite);
1853
+ const run = async () => {
1854
+ // A tool that writes runs only once the builder read the input and said so.
1855
+ const ask = writeAsk(tool, sampleInput);
1856
+ if (ask && !(await confirmWrite(ask)))
1857
+ return;
1858
+ setPending(true);
1859
+ try {
1860
+ setResult(await runToolProbe(tool, sampleInput, credential, guardrails));
1861
+ }
1862
+ finally {
1863
+ setPending(false);
1864
+ }
1865
+ };
1866
+ return (_jsxs(InspectorSection, { title: "Test", note: http ? sectionNote('tool.test.http', { method }) : sectionNote('tool.test.mcp'), children: [http && _jsx(SampleInputRow, { value: sampleInput, onChange: setTypedInput }), (tool.authType ?? 'none') !== 'none' && (_jsx(TestCredentialRow, { value: credential, onChange: setCredential })), _jsx(TestConnectionButton, { isLoading: pending, onClick: () => {
1867
+ void run();
1868
+ } }), result && (_jsx(Banner, { status: probeStatus(result), title: probeTitle(result, tool), description: probeDetails(result) }))] }));
1869
+ }
1870
+ /** The tool types offered: an agent tool only where there are agents to run. */
1871
+ function toolTypeSegments(workspace) {
1872
+ return workspace
1873
+ ? TOOL_TYPE_SEGMENTS
1874
+ : TOOL_TYPE_SEGMENTS.filter((segment) => segment.value !== 'agent');
1875
+ }
1876
+ /** Renames a tool; the T2 loader names this tool, so it follows the rename. */
1877
+ function renameToolSpec(draft, toolKey, from, toolName) {
1878
+ return {
1879
+ ...draft,
1880
+ tools: draft.tools.t2Loader === from.trim()
1881
+ ? { ...draft.tools, t2Loader: toolName.trim() }
1882
+ : draft.tools,
1883
+ toolSpecs: draft.toolSpecs.map((candidate) => candidate.key === toolKey ? { ...candidate, toolName } : candidate),
1884
+ };
1885
+ }
1886
+ /** Removes a tool, and the T2 loader with it when it names the tool. */
1887
+ function removeToolSpec(draft, toolKey, name) {
1888
+ return {
1889
+ ...draft,
1890
+ tools: draft.tools.t2Loader === name.trim() ? { ...draft.tools, t2Loader: '' } : draft.tools,
1891
+ toolSpecs: draft.toolSpecs.filter((candidate) => candidate.key !== toolKey),
1892
+ };
1893
+ }
1894
+ /** The tool's type, from the types on offer here. */
1895
+ function ToolTypeRow({ tool, set }) {
1896
+ const workspace = useContext(WorkspaceContext);
1897
+ return (_jsx(SegmentedRow, { label: "Type", path: "registerTool.type", field: "toolType", value: tool.toolType, segments: toolTypeSegments(workspace), onChange: (toolType) => {
1898
+ set({ toolType });
1899
+ } }));
1900
+ }
1901
+ /** What the tool is: its name, type, description, and category. */
1902
+ function ToolSection({ draft, setDraft, tool, set, }) {
1903
+ return (_jsxs(InspectorSection, { note: draft.identity.profileType === 'host' ? sectionNote('tool.host') : undefined, children: [_jsx(TextRow, { label: "Name", path: "registerTool.name", field: "toolName", isRequired: true, value: tool.toolName, placeholder: "search_flights", onChange: (toolName) => {
1904
+ setDraft((current) => renameToolSpec(current, tool.key, tool.toolName, toolName));
1905
+ } }), _jsx(ToolTypeRow, { tool: tool, set: set }), _jsx(TextAreaRow, { label: "Description", path: "registerTool.description", field: "description", isRequired: true, value: tool.description, placeholder: "Finds flights between two airports on a date.", onChange: (description) => {
1906
+ set({ description });
1907
+ } }), _jsx(TextRow, { label: "Category", path: "registerTool.category", field: "category", value: tool.category, placeholder: "studio", onChange: (category) => {
1908
+ set({ category });
1909
+ } })] }));
1910
+ }
1911
+ /** The tool's input and output schemas. */
1912
+ function ContractSection({ tool, set }) {
1913
+ return (_jsxs(InspectorSection, { title: "Contract", children: [_jsx(TextAreaRow, { label: "Input", path: "registerTool.inputSchema", field: "inputJson", value: tool.inputJson, rows: 8, hasSpellCheck: false, onChange: (inputJson) => {
1914
+ set({ inputJson });
1915
+ } }), _jsx(TextAreaRow, { label: "Output", path: "registerTool.outputSchema", field: "outputJson", value: tool.outputJson, rows: 8, hasSpellCheck: false, onChange: (outputJson) => {
1916
+ set({ outputJson });
1917
+ } })] }));
1918
+ }
1919
+ /** One label: a line of wording, or a list with a line each, read top to bottom. */
1920
+ function ActivityLabelRow({ label, field, value, schemas, onChange, }) {
1921
+ const text = value ?? '';
1922
+ return (_jsx(TextAreaRow, { label: label, path: `registerTool.labels.${field}`, field: field, value: text, rows: Math.max(1, text.split('\n').length), hint: placeholderHint(schemas), onChange: onChange }));
1923
+ }
1924
+ /** What the chat says while the tool runs, once it's done, when it fails and when it asks. */
1925
+ function ActivitySection({ tool, set }) {
1926
+ return (_jsxs(InspectorSection, { title: "Activity", path: "registerTool.labels", children: [_jsx(ActivityLabelRow, { label: "Running", field: "activity", value: tool.activity, schemas: [tool.inputJson], onChange: (activity) => {
1927
+ set({ activity });
1928
+ } }), _jsx(ActivityLabelRow, { label: "Done", field: "activityPast", value: tool.activityPast, schemas: [tool.inputJson, tool.outputJson], onChange: (activityPast) => {
1929
+ set({ activityPast });
1930
+ } }), _jsx(ActivityLabelRow, { label: "Failed", field: "activityFailed", value: tool.activityFailed, schemas: [tool.inputJson], onChange: (activityFailed) => {
1931
+ set({ activityFailed });
1932
+ } }), _jsx(ActivityLabelRow, { label: "Asking", field: "request", value: tool.request, schemas: [tool.inputJson], onChange: (request) => {
1933
+ set({ request });
1934
+ } })] }));
1935
+ }
1936
+ /** What a function tool returns in the studio, and whether the page answers it on a host. */
1937
+ function StubSection({ tool, set }) {
1938
+ return (_jsxs(InspectorSection, { title: "Stub", path: "studio.stubOutput", children: [_jsx(SwitchRow, { label: "Page answers", path: "registerTool.answeredBy", value: tool.answeredBy === 'page', onChange: (page) => {
1939
+ set({ answeredBy: page ? 'page' : undefined });
1940
+ } }), _jsx(TextAreaRow, { label: "Returns", path: "studio.stubOutput", field: "stubOutputJson", value: tool.stubOutputJson ?? '', rows: 6, hasSpellCheck: false, hint: "Left blank, a stand-in built from the output schema.", onChange: (stubOutputJson) => {
1941
+ set({ stubOutputJson });
1942
+ } })] }));
1943
+ }
1944
+ /** The static headers HTTP and MCP tools both send. */
1945
+ function HeadersRow({ tool, set }) {
1946
+ return (_jsx(TextAreaRow, { label: "Headers", path: "registerTool.headers", field: "headersJson", value: tool.headersJson ?? '', rows: 4, hasSpellCheck: false, placeholder: HEADERS_PLACEHOLDER, onChange: (headersJson) => {
1947
+ set({ headersJson });
1948
+ } }));
1949
+ }
1950
+ /** An HTTP tool's request, and how its input maps onto it. */
1951
+ function HttpSections({ tool, set }) {
1952
+ return (_jsxs(_Fragment, { children: [_jsx(HttpRequestSection, { tool: tool, set: set }), _jsx(HttpMappingSection, { tool: tool, set: set })] }));
1953
+ }
1954
+ function HttpRequestSection({ tool, set }) {
1955
+ return (_jsxs(InspectorSection, { title: "Request", note: sectionNote('tool.headers'), children: [_jsx(TextRow, { label: "Endpoint", path: "registerTool.endpoint", field: "endpoint", isRequired: true, value: tool.endpoint ?? '', placeholder: "https://api.example.com/flights/{id}", onChange: (endpoint) => {
1956
+ set({ endpoint });
1957
+ } }), _jsx(SegmentedRow, { label: "Method", path: "registerTool.method", field: "method", value: tool.method ?? HTTP_METHODS[0], segments: METHOD_SEGMENTS, onChange: (method) => {
1958
+ set({ method });
1959
+ } }), _jsx(HeadersRow, { tool: tool, set: set })] }));
1960
+ }
1961
+ function HttpMappingSection({ tool, set }) {
1962
+ return (_jsxs(InspectorSection, { title: "Mapping", path: "registerTool.mapping", children: [_jsx(NamesRow, { label: "Path", path: "registerTool.mapping.pathParams", field: "pathParams", value: tool.pathParams ?? [], placeholder: "Names in {braces}", onChange: (pathParams) => {
1963
+ set({ pathParams });
1964
+ } }), _jsx(NamesRow, { label: "Query", path: "registerTool.mapping.queryParams", field: "queryParams", value: tool.queryParams ?? [], placeholder: "None", onChange: (queryParams) => {
1965
+ set({ queryParams });
1966
+ } }), _jsx(TextRow, { label: "Body", path: "registerTool.mapping.bodyParam", field: "bodyParam", value: tool.bodyParam ?? '', hint: "None", onChange: (bodyParam) => {
1967
+ set({ bodyParam });
1968
+ } })] }));
1969
+ }
1970
+ /** The agent an agent tool runs, and how often a turn may call it. */
1971
+ function AgentSection({ tool, set, workspace, }) {
1972
+ return (_jsxs(InspectorSection, { title: "Agent", children: [_jsx(ChoiceRow, { label: "Runs", path: "registerTool.profile", field: "agentKey", isRequired: true, value: tool.agentKey ?? '', options: otherAgents(workspace.agents, workspace.self, (agent) => CALLABLE_TYPES.has(agent.type)), onChange: (agentKey) => {
1973
+ const agentId = workspace.agents.find((agent) => agent.key === agentKey)?.agentId ?? '';
1974
+ set(agentToolTarget(tool, agentKey, agentId));
1975
+ } }), _jsx(NumberRow, { label: "Calls per turn", path: "registerTool.maxCallsPerTurn", field: "maxCallsPerTurn", value: tool.maxCallsPerTurn ?? null, min: 1, isIntegerOnly: true, onChange: (maxCallsPerTurn) => {
1976
+ set({ maxCallsPerTurn });
1977
+ } })] }));
1978
+ }
1979
+ /** An MCP tool's server and the tool it calls there. */
1980
+ function McpServerSection({ tool, set }) {
1981
+ return (_jsxs(InspectorSection, { title: "Server", note: sectionNote('tool.headers'), children: [_jsx(TextRow, { label: "Server", path: "registerTool.serverUrl", field: "serverUrl", isRequired: true, value: tool.serverUrl ?? '', placeholder: "https://mcp.example.com/mcp", onChange: (serverUrl) => {
1982
+ set({ serverUrl });
1983
+ } }), _jsx(TextRow, { label: "Remote tool", path: "registerTool.mcpToolName", field: "mcpToolName", isRequired: true, value: tool.mcpToolName ?? '', placeholder: "search_flights", onChange: (mcpToolName) => {
1984
+ set({ mcpToolName });
1985
+ } }), _jsx(HeadersRow, { tool: tool, set: set })] }));
1986
+ }
1987
+ /** Where an authenticated tool's credential comes from, how it's sent, and what signed out does. */
1988
+ function AuthCredentialRows({ tool, authType, set, }) {
1989
+ const inUrl = authType === 'api_key' && Boolean(tool.authQueryParam?.trim());
1990
+ return (_jsxs(_Fragment, { children: [_jsx(TextRow, { label: "Slot", path: "registerTool.auth.slot", field: "authSlot", value: tool.authSlot ?? '', placeholder: "default", onChange: (authSlot) => {
1991
+ set({ authSlot });
1992
+ } }), authType === 'api_key' ? (_jsx(TextRow, { label: "Query parameter", path: "registerTool.auth.queryParam", field: "authQueryParam", value: tool.authQueryParam ?? '', onChange: (queryParam) => {
1993
+ // The key goes in one place: in the URL, the header rows have nothing to say.
1994
+ set(queryParam
1995
+ ? {
1996
+ authQueryParam: queryParam,
1997
+ authHeaderName: undefined,
1998
+ authHeaderPrefix: undefined,
1999
+ }
2000
+ : { authQueryParam: undefined });
2001
+ } })) : null, inUrl ? null : (_jsxs(_Fragment, { children: [_jsx(TextRow, { label: "Header", path: "registerTool.auth.headerName", field: "authHeaderName", value: tool.authHeaderName ?? '', placeholder: "Authorization", onChange: (authHeaderName) => {
2002
+ set({ authHeaderName });
2003
+ } }), _jsx(TextRow, { label: "Prefix", path: "registerTool.auth.headerPrefix", field: "authHeaderPrefix", value: tool.authHeaderPrefix ?? '', placeholder: AUTH_HEADER_PREFIX[authType] || undefined, hint: "None", onChange: (prefix) => {
2004
+ // Blank is the kernel's prefix for the type, which the placeholder shows.
2005
+ set({ authHeaderPrefix: prefix || undefined });
2006
+ } })] })), _jsx(SegmentedRow, { label: "Signed out", path: "registerTool.auth.onUnauthenticated", field: "authUnauthenticated", value: tool.authUnauthenticated ?? 'gate', segments: UNAUTHENTICATED_SEGMENTS, onChange: (authUnauthenticated) => {
2007
+ set({ authUnauthenticated });
2008
+ } })] }));
2009
+ }
2010
+ /** An OAuth tool's scopes, client id, and redirect. */
2011
+ function OAuthRows({ tool, set }) {
2012
+ return (_jsxs(_Fragment, { children: [_jsx(NamesRow, { label: "Scopes", path: "registerTool.auth.scopes", field: "authScopes", value: tool.authScopes ?? [], placeholder: "None", onChange: (authScopes) => {
2013
+ set({ authScopes });
2014
+ } }), _jsx(TextRow, { label: "Client id", path: "registerTool.auth.clientId", field: "authClientId", value: tool.authClientId ?? '', onChange: (authClientId) => {
2015
+ set({ authClientId });
2016
+ } }), _jsx(TextRow, { label: "Redirect", path: "registerTool.auth.redirectUri", field: "authRedirectUri", value: tool.authRedirectUri ?? '', placeholder: "https://example.com/oauth/callback", onChange: (authRedirectUri) => {
2017
+ set({ authRedirectUri });
2018
+ } })] }));
2019
+ }
2020
+ /** How a remote tool authenticates. */
2021
+ function AuthSection({ tool, set }) {
2022
+ const authType = tool.authType ?? 'none';
2023
+ return (_jsxs(InspectorSection, { path: "registerTool.auth", note: sectionNote('tool.auth'), children: [_jsx(SegmentedRow, { label: "Type", path: "studio.authType", field: "authType", value: authType, segments: AUTH_TYPE_SEGMENTS, onChange: (next) => {
2024
+ set({ authType: next });
2025
+ } }), authType !== 'none' && _jsx(AuthCredentialRows, { tool: tool, authType: authType, set: set }), authType === 'oauth2' && _jsx(OAuthRows, { tool: tool, set: set })] }));
2026
+ }
2027
+ /** What the studio does about a tool that writes, when that is not what Permission says. */
2028
+ function studioAskNote(asks, tool) {
2029
+ if (asks.asked.includes(tool))
2030
+ return 'In the studio this tool asks before every run, because it writes. Your application runs it as Permission says.';
2031
+ if (asks.inside.includes(tool))
2032
+ return 'This tool writes, and the studio cannot make it ask: a called agent runs it, and a question inside a called agent has no one to answer it. The call to that agent asks instead.';
2033
+ return undefined;
2034
+ }
2035
+ /** Who may call the tool, whether it asks first, when it loads and on which paths. */
2036
+ function PolicySection({ draft, tool, set, }) {
2037
+ const note = studioAskNote(useContext(ProjectAsks), tool.toolName);
2038
+ return (_jsxs(InspectorSection, { title: "Policy", note: note, children: [_jsx(SegmentedRow, { label: "Access", path: "registerTool.access", field: "access", value: tool.access, segments: ACCESS_SEGMENTS, onChange: (access) => {
2039
+ set({ access });
2040
+ } }), _jsx(SegmentedRow, { label: "Permission", path: "registerTool.permission", field: "permission", value: tool.permission, segments: PERMISSION_SEGMENTS, onChange: (permission) => {
2041
+ set({ permission });
2042
+ } }), _jsx(SegmentedRow, { label: "Load tier", path: "registerTool.loadTier", field: "loadTier", value: tool.loadTier, segments: LOAD_TIER_SEGMENTS, warning: loadTierWarning(draft, tool.loadTier), onChange: (loadTier) => {
2043
+ set({ loadTier });
2044
+ } }), _jsx(NamesRow, { label: "Paths", path: "registerTool.paths", field: "paths", value: tool.paths, placeholder: "Every path (*)", onChange: (paths) => {
2045
+ set({ paths });
2046
+ } })] }));
2047
+ }
2048
+ function ToolSpecEditor({ draft, setDraft, toolKey, onSelect, }) {
2049
+ const workspace = useContext(WorkspaceContext);
2050
+ const tool = draft.toolSpecs.find((candidate) => candidate.key === toolKey);
2051
+ if (!tool)
2052
+ return null;
2053
+ const set = (change) => {
2054
+ setDraft((current) => ({
2055
+ ...current,
2056
+ toolSpecs: current.toolSpecs.map((candidate) => candidate.key === toolKey ? { ...candidate, ...change } : candidate),
2057
+ }));
2058
+ };
2059
+ const remote = tool.toolType === 'http' || tool.toolType === 'mcp';
2060
+ return (_jsxs(_Fragment, { children: [_jsx(ToolSection, { draft: draft, setDraft: setDraft, tool: tool, set: set }), _jsx(ContractSection, { tool: tool, set: set }), _jsx(ActivitySection, { tool: tool, set: set }), tool.toolType === 'function' && _jsx(StubSection, { tool: tool, set: set }), tool.toolType === 'http' && _jsx(HttpSections, { tool: tool, set: set }), tool.toolType === 'agent' && workspace && (_jsx(AgentSection, { tool: tool, set: set, workspace: workspace })), tool.toolType === 'mcp' && _jsx(McpServerSection, { tool: tool, set: set }), remote && _jsx(AuthSection, { tool: tool, set: set }), remote && _jsx(ToolTest, { tool: tool, guardrails: draft.guardrails }, tool.key), _jsx(PolicySection, { draft: draft, tool: tool, set: set }), _jsx(Section, { variant: "transparent", padding: 3, children: _jsx(Button, { label: "Remove tool", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconTrash, size: "sm" }), onClick: () => {
2061
+ setDraft((current) => removeToolSpec(current, toolKey, tool.toolName));
2062
+ onSelect('tools');
2063
+ } }) })] }));
2064
+ }
2065
+ const LABEL_AREAS = [
2066
+ {
2067
+ prefix: 'error',
2068
+ title: 'Errors',
2069
+ note: 'When a turn fails.',
2070
+ icon: IconAlertTriangle,
2071
+ audience: 'visitor',
2072
+ },
2073
+ {
2074
+ prefix: 'attachments',
2075
+ title: 'Attachments',
2076
+ note: "When a file can't be sent.",
2077
+ icon: IconPaperclip,
2078
+ audience: 'visitor',
2079
+ },
2080
+ {
2081
+ prefix: 'voice',
2082
+ title: 'Voice',
2083
+ note: "When a voice note can't be recorded or used.",
2084
+ icon: IconMicrophone,
2085
+ audience: 'visitor',
2086
+ },
2087
+ {
2088
+ prefix: 'session',
2089
+ title: 'Session',
2090
+ note: 'Waits, approvals, and signing in.',
2091
+ icon: IconHourglass,
2092
+ audience: 'visitor',
2093
+ },
2094
+ {
2095
+ prefix: 'stop',
2096
+ title: 'Stops',
2097
+ note: 'Why a reply ended short.',
2098
+ icon: IconPlayerStop,
2099
+ audience: 'visitor',
2100
+ },
2101
+ {
2102
+ prefix: 'composer',
2103
+ title: 'Composer',
2104
+ note: 'The model and effort pickers.',
2105
+ icon: IconAdjustmentsHorizontal,
2106
+ audience: 'visitor',
2107
+ },
2108
+ {
2109
+ prefix: 'live',
2110
+ title: 'Live',
2111
+ note: 'When a live session ends.',
2112
+ icon: IconHeadset,
2113
+ audience: 'visitor',
2114
+ },
2115
+ {
2116
+ prefix: 'quota',
2117
+ title: 'Quota',
2118
+ note: 'When the daily limit is reached.',
2119
+ icon: IconGauge,
2120
+ audience: 'visitor',
2121
+ },
2122
+ {
2123
+ prefix: 'tool',
2124
+ title: 'Tools',
2125
+ note: "When a tool call can't run.",
2126
+ icon: IconTool,
2127
+ audience: 'model',
2128
+ },
2129
+ {
2130
+ prefix: 'repair',
2131
+ title: 'Repair',
2132
+ note: 'Fixing a reply that failed its schema.',
2133
+ icon: IconBandage,
2134
+ audience: 'model',
2135
+ },
2136
+ {
2137
+ prefix: 'egress',
2138
+ title: 'Egress',
2139
+ note: 'A reply that fails its check, sent back or refused.',
2140
+ icon: IconSend,
2141
+ audience: 'model',
2142
+ },
2143
+ {
2144
+ prefix: 'continue',
2145
+ title: 'Resumption',
2146
+ note: 'Picking up a reply that stopped short.',
2147
+ icon: IconPlayerTrackNext,
2148
+ audience: 'model',
2149
+ },
2150
+ {
2151
+ prefix: 'canary',
2152
+ title: 'Canary',
2153
+ note: "The note that binds each turn's canary.",
2154
+ icon: IconFeather,
2155
+ audience: 'model',
2156
+ },
2157
+ {
2158
+ prefix: 'taint',
2159
+ title: 'Untrusted content',
2160
+ note: 'Why a tool call was refused after reading it.',
2161
+ icon: IconBiohazard,
2162
+ audience: 'model',
2163
+ },
2164
+ {
2165
+ prefix: 'advisory',
2166
+ title: 'Advisories',
2167
+ note: 'Notes beside content that tries to direct it.',
2168
+ icon: IconInfoCircle,
2169
+ audience: 'model',
2170
+ },
2171
+ ];
2172
+ const LABEL_AUDIENCES = [
2173
+ {
2174
+ value: 'visitor',
2175
+ label: 'Visitor reads',
2176
+ icon: IconUser,
2177
+ note: 'Shown in the chat when something fails or needs a wait.',
2178
+ },
2179
+ {
2180
+ value: 'model',
2181
+ label: 'Model reads',
2182
+ icon: IconInputAi,
2183
+ note: 'Put in front of the model; the visitor never sees these.',
2184
+ },
2185
+ ];
2186
+ /** Row names for keys whose own name reads as code. */
2187
+ const LABEL_TITLES = {
2188
+ 'attachments.mime_not_allowed': 'File type not allowed',
2189
+ 'attachments.limits_unconfigured': 'Files turned off',
2190
+ 'quota.exhausted': 'Limit reached',
2191
+ };
2192
+ /** `rate_limit` → "Rate limit". */
2193
+ function labelTitle(key) {
2194
+ const named = LABEL_TITLES[key];
2195
+ if (named)
2196
+ return named;
2197
+ const words = key.slice(key.indexOf('.') + 1).replaceAll('_', ' ');
2198
+ return words.charAt(0).toUpperCase() + words.slice(1);
2199
+ }
2200
+ /** The kernel's line for `key`, worded with the draft's own limits where it takes them. */
2201
+ function labelPlaceholder(draft, key) {
2202
+ const { inputs, guardrails } = draft;
2203
+ const text = labelDefault(key, {
2204
+ maxFiles: inputs.maxFiles ?? '{maxFiles}',
2205
+ maxBytes: inputs.maxBytes ?? Number.NaN,
2206
+ maxTurnBytes: inputs.maxTurnBytes ?? Number.NaN,
2207
+ perDay: guardrails.quotaPerDay ?? '{perDay}',
2208
+ });
2209
+ // A size the draft leaves unset formats as NaN: show the placeholder the template takes instead.
2210
+ return text.replace('NaN MB', key === 'attachments.turn_too_large' ? '{maxTurnBytes}' : '{maxBytes}');
2211
+ }
2212
+ /**
2213
+ * The lines that also sit beside their setting: one value, edited from either place. `isOn` is when
2214
+ * the kernel uses the line; off, Labels links to the setting that turns it on.
2215
+ */
2216
+ const SHARED_LABELS = {
2217
+ 'continue.instruction': {
2218
+ setting: 'Resumption',
2219
+ read: (draft) => draft.turnBehaviour.continueInstruction,
2220
+ write: (setDraft, continueInstruction) => {
2221
+ patch(setDraft, 'turnBehaviour')({ continueInstruction });
2222
+ },
2223
+ isOn: (draft) => takesContinueInstruction(draft) && draft.turnBehaviour.resumeEnabled,
2224
+ },
2225
+ 'canary.bind_note': {
2226
+ setting: 'Canary',
2227
+ read: (draft) => draft.guardrails.canaryBindNote,
2228
+ write: (setDraft, canaryBindNote) => {
2229
+ patch(setDraft, 'guardrails')({ canaryBindNote });
2230
+ },
2231
+ isOn: plantsCanary,
2232
+ },
2233
+ 'quota.exhausted': {
2234
+ setting: 'Daily cap',
2235
+ read: (draft) => draft.guardrails.quotaMessage,
2236
+ write: (setDraft, quotaMessage) => {
2237
+ patch(setDraft, 'guardrails')({ quotaMessage });
2238
+ },
2239
+ isOn: (draft) => draft.guardrails.quotaEnabled,
2240
+ },
2241
+ 'repair.default_guidance': {
2242
+ setting: 'Repair',
2243
+ read: (draft) => draft.outputs.repairGuidance,
2244
+ write: (setDraft, repairGuidance) => {
2245
+ patch(setDraft, 'outputs')({ repairGuidance });
2246
+ },
2247
+ isOn: (draft) => draft.outputs.validationEnabled,
2248
+ },
2249
+ 'egress.default_repair_guidance': {
2250
+ setting: 'Blocked reply',
2251
+ read: (draft) => draft.guardrails.egressRepairGuidance,
2252
+ write: (setDraft, egressRepairGuidance) => {
2253
+ patch(setDraft, 'guardrails')({ egressRepairGuidance });
2254
+ },
2255
+ isOn: (draft) => draft.guardrails.blockedReplyOnBlock !== 'refuse',
2256
+ },
2257
+ 'egress.refusal': {
2258
+ setting: 'Blocked reply',
2259
+ read: (draft) => draft.guardrails.egressRefusal,
2260
+ write: (setDraft, egressRefusal) => {
2261
+ patch(setDraft, 'guardrails')({ egressRefusal });
2262
+ },
2263
+ isOn: (draft) => draft.guardrails.blockedReplyOnBlock === 'refuse',
2264
+ },
2265
+ };
2266
+ /** The text a line holds in the draft: its own setting's field, or Labels'. */
2267
+ function labelValue(draft, key) {
2268
+ return SHARED_LABELS[key]?.read(draft) ?? draft.labels[key] ?? '';
2269
+ }
2270
+ /** A line that also sits beside its setting; while that setting is off, a link to turn it on. */
2271
+ function SharedLabelRow({ labelKey, facet, draft, setDraft, onSelect, }) {
2272
+ const shared = SHARED_LABELS[labelKey];
2273
+ if (!shared)
2274
+ return null;
2275
+ return (_jsxs(VStack, { gap: 1, children: [_jsx(TextAreaRow, { label: labelTitle(labelKey), path: `labels.${labelKey}`, rows: 2, value: shared.read(draft), placeholder: labelPlaceholder(draft, labelKey), onChange: (text) => {
2276
+ shared.write(setDraft, text);
2277
+ } }), !shared.isOn(draft) && (_jsxs(HStack, { gap: 1, align: "center", children: [_jsxs(Text, { type: "supporting", children: [shared.setting, " is off."] }), _jsx(Button, { label: `Open ${profileGraphFacet(facet)?.label ?? facet}`, variant: "ghost", size: "sm", onClick: () => {
2278
+ // A section left out of the profile has no node to open until it's back in.
2279
+ setDraft((current) => includeFacet(current, facet));
2280
+ onSelect(facet);
2281
+ } })] }))] }));
2282
+ }
2283
+ /** Every line the kernel says, by area; left blank, a line keeps the kernel's own. */
2284
+ function LabelsEditor({ draft, setDraft, onSelect, }) {
2285
+ const [audience, setAudience] = useState('visitor');
2286
+ const [query, setQuery] = useState('');
2287
+ const needle = query.trim().toLowerCase();
2288
+ const areas = labelAreas(draft, needle, audience);
2289
+ const note = LABEL_AUDIENCES.find((entry) => entry.value === audience)?.note;
2290
+ return (_jsx(Section, { variant: "transparent", padding: 3, children: _jsxs(VStack, { gap: 3, children: [_jsx(TextInput, { label: "Search labels", isLabelHidden: true, placeholder: "Search names, defaults, and text", value: query, onChange: setQuery, startIcon: IconSearch, hasClear: true }), !needle && _jsx(LabelAudiencePicker, { value: audience, onChange: setAudience }), !needle && _jsx(Text, { type: "supporting", children: note }), needle && areas.length === 0 && (_jsxs(Text, { type: "supporting", children: ["No labels match \u201C", query.trim(), "\u201D."] })), _jsx(CollapsibleGroup
2291
+ // Searching opens every area with a match; clearing the search goes back to all closed.
2292
+ , { type: "multiple", hasDividers: true, density: "compact", defaultValue: needle ? areas.map((area) => area.prefix) : undefined, children: areas.map((area) => (_jsx(Collapsible, { value: area.prefix, trigger: _jsx(LabelAreaTrigger, { area: area }), children: _jsx(LabelAreaLines, { keys: area.keys, draft: draft, setDraft: setDraft, onSelect: onSelect }) }, area.prefix))) }, needle ? `search:${areas.map((area) => area.prefix).join()}` : audience)] }) }));
2293
+ }
2294
+ /**
2295
+ * The areas on show, each with its lines. A search spans both readers: a line matches on its area,
2296
+ * its name, its default, or its text.
2297
+ */
2298
+ function labelAreas(draft, needle, audience) {
2299
+ return LABEL_AREAS.filter((area) => needle || area.audience === audience)
2300
+ .map((area) => {
2301
+ const all = LABEL_KEYS.filter((key) => key.startsWith(`${area.prefix}.`));
2302
+ const keys = needle
2303
+ ? all.filter((key) => [
2304
+ area.title,
2305
+ labelTitle(key),
2306
+ labelPlaceholder(draft, key),
2307
+ labelValue(draft, key),
2308
+ ].some((text) => text.toLowerCase().includes(needle)))
2309
+ : all;
2310
+ const edited = all.filter((key) => labelValue(draft, key)).length;
2311
+ return { ...area, keys, edited };
2312
+ })
2313
+ .filter((area) => area.keys.length > 0);
2314
+ }
2315
+ /** Who reads the lines on show: the visitor, or the model. */
2316
+ function LabelAudiencePicker({ value, onChange, }) {
2317
+ return (_jsx(SegmentedControl, { label: "Who reads it", size: "sm", layout: "fill", value: value, onChange: (next) => {
2318
+ const picked = LABEL_AUDIENCES.find((entry) => entry.value === next);
2319
+ if (picked)
2320
+ onChange(picked.value);
2321
+ }, children: LABEL_AUDIENCES.map((entry) => (_jsx(Tooltip, { content: entry.label, children: _jsx(SegmentedControlItem, { value: entry.value, label: entry.label, isLabelHidden: true, icon: _jsx(Icon, { icon: entry.icon, size: "sm" }) }) }, entry.value))) }));
2322
+ }
2323
+ /** An area's heading: its icon, title and note, and how many of its lines are edited. */
2324
+ function LabelAreaTrigger({ area }) {
2325
+ return (_jsxs(HStack, { gap: 2, align: "center", children: [_jsx(Icon, { icon: area.icon, size: "sm", color: "secondary" }), _jsx(StackItem, { size: "fill", children: _jsxs(VStack, { gap: 0, children: [_jsx(Text, { type: "supporting", weight: "semibold", color: "primary", children: area.title }), _jsx(Text, { type: "supporting", color: "secondary", children: area.note })] }) }), area.edited > 0 && _jsx(Badge, { variant: "info", label: `${String(area.edited)} edited` })] }));
2326
+ }
2327
+ /** An area's lines; one that also sits beside its setting edits that setting's field. */
2328
+ function LabelAreaLines({ keys, draft, setDraft, onSelect, }) {
2329
+ const set = patch(setDraft, 'labels');
2330
+ return (_jsx(VStack, { gap: 3, paddingBlock: 2, children: keys.map((key) => {
2331
+ const facet = INLINE_LABELS[key];
2332
+ if (facet)
2333
+ return (_jsx(SharedLabelRow, { labelKey: key, facet: facet, draft: draft, setDraft: setDraft, onSelect: onSelect }, key));
2334
+ return (_jsx(TextAreaRow, { label: labelTitle(key), path: `labels.${key}`, field: key, rows: 2, value: draft.labels[key] ?? '', placeholder: labelPlaceholder(draft, key), onChange: (text) => {
2335
+ set({ [key]: text });
2336
+ } }, key));
2337
+ }) }));
2338
+ }
2339
+ const QUESTION_TYPE_SEGMENTS = [
2340
+ { value: 'choice', label: 'Choice', icon: QUESTION_TYPE_ICON.choice },
2341
+ { value: 'score', label: 'Score', icon: QUESTION_TYPE_ICON.score },
2342
+ { value: 'noul', label: 'Noul', icon: QUESTION_TYPE_ICON.noul },
2343
+ { value: 'box', label: 'Box', icon: QUESTION_TYPE_ICON.box },
2344
+ ];
2345
+ /** What a question's rows are called, and what a new one's text says. */
2346
+ const CRITERIA_COPY = {
2347
+ choice: {
2348
+ title: 'Options',
2349
+ row: 'Option',
2350
+ note: 'Each answer it may pick, and when to pick it.',
2351
+ },
2352
+ score: { title: 'Levels', row: 'Level', note: 'The scale from 0 up, each level described.' },
2353
+ noul: { title: 'Criteria', row: 'Criterion', note: 'Optional named things the number weighs.' },
2354
+ box: { title: 'Criteria', row: 'Criterion', note: 'A box takes no criteria.' },
2355
+ };
2356
+ /** What a question is called in its row and on its page, before it has a name. */
2357
+ const questionName = (question, index) => question.id.trim() || `Question ${String(index + 1)}`;
2358
+ /** A question's issues, wherever on its page they are. */
2359
+ function useQuestionIssues() {
2360
+ const issues = useContext(NodeIssues);
2361
+ return (index) => issues
2362
+ .filter((issue) => issue.field === 'questions' && issue.index === index)
2363
+ .map((issue) => issue.message);
2364
+ }
2365
+ /**
2366
+ * One question's row: its answer type's icon, its name, and its instructions, with how many options
2367
+ * or levels it has, or what is wrong with it. A click anywhere on it presses its Open button.
2368
+ */
2369
+ function QuestionRow({ question, name, issues, onOpen, }) {
2370
+ const button = useRef(null);
2371
+ const segment = QUESTION_TYPE_SEGMENTS.find((each) => each.value === question.type);
2372
+ const open = `Open ${name}: its answer and instructions`;
2373
+ const listed = question.type !== 'box' && question.criteria.length > 0;
2374
+ const trailing = issues.length > 0 ? (_jsx(Tooltip, { content: issues.join(' '), children: _jsx(Badge, { variant: "error", label: String(issues.length) }) })) : (listed && (_jsx(Tooltip, { content: question.criteria
2375
+ .map((criterion) => criterion.label.trim() || criterion.text.trim())
2376
+ .join(', '), children: _jsx(Token, { label: `${String(question.criteria.length)} ${CRITERIA_COPY[question.type].title.toLowerCase()}`, size: "sm" }) })));
2377
+ return (_jsx(ListItem, { label: name, description: question.instructions.trim() || undefined, interactiveRef: button, startContent: _jsx(Tooltip, { content: `${segment?.label ?? question.type}: ${fieldMeta('decision.questions.type')?.optionDescriptions?.[question.type] ?? ''}`, children: _jsx(Icon, { icon: QUESTION_TYPE_ICON[question.type], size: "sm", color: "secondary" }) }), endContent: _jsxs(HStack, { gap: 1, vAlign: "center", children: [trailing, _jsx(Tooltip, { content: open, children: _jsx(IconButton, { ref: button, label: open, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconChevronRight, size: "sm" }), onClick: onOpen }) })] }) }));
2378
+ }
2379
+ /** Every question on one line, in the order they are answered. Each opens its own page. */
2380
+ function QuestionList({ questions, onOpen, }) {
2381
+ const issuesAt = useQuestionIssues();
2382
+ return (_jsx(List, { density: "compact", children: questions.map((question, index) => (_jsx(QuestionRow, { question: question, name: questionName(question, index), issues: issuesAt(index), onOpen: () => {
2383
+ onOpen(question.key);
2384
+ } }, question.key))) }));
2385
+ }
2386
+ /** One question's page: its name and answer type, what to ask, and its options or levels. */
2387
+ function QuestionPage({ question, index, count, onChange, onMove, onRemove, images, onBack, }) {
2388
+ const status = useFieldStatus()('questions', index);
2389
+ return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: questionName(question, index), back: "Back to questions", trailing: _jsx(QuestionActions, { name: questionName(question, index), index: index, count: count, onMove: onMove, onRemove: onRemove }), onBack: onBack }), _jsxs(InspectorSection, { children: [_jsx(TextRow, { label: "Name", path: "decision.questions.id", status: status, value: question.id, placeholder: "verdict", onChange: (id) => {
2390
+ onChange({ id });
2391
+ } }), _jsx(SegmentedRow, { label: "Answer", path: "decision.questions.type", value: question.type, segments: QUESTION_TYPE_SEGMENTS, selectorPast: 0, onChange: (type) => {
2392
+ // Options carry over between choice and number; a score's levels and a box's none are different lists.
2393
+ const listed = (kind) => kind === 'choice' || kind === 'noul';
2394
+ const keeps = listed(type) && listed(question.type);
2395
+ onChange({ type, ...(keeps ? {} : { criteria: newCriteria(type) }) });
2396
+ } }), _jsx(TextAreaRow, { label: "Instructions", path: "decision.questions.instructions", rows: 3, value: question.instructions, hint: "What should the model decide, given the state?", onChange: (instructions) => {
2397
+ onChange({ instructions });
2398
+ } }), question.type === 'box' && (_jsx(ChoiceRow, { label: "In image", path: "decision.questions.image", value: question.image ?? '', options: boxImages(question, images), emptyText: "Add an image under Images, or name a state field in backticks.", onChange: (image) => {
2399
+ onChange({ image: image || undefined });
2400
+ } }))] }), question.type !== 'box' && _jsx(CriteriaList, { question: question, onChange: onChange })] }));
2401
+ }
2402
+ /** What a box can be in: the decision's own images, then the state fields its instructions name. */
2403
+ function boxImages(question, images) {
2404
+ const fields = [...question.instructions.matchAll(/\x60([^\x60\s]+)\x60/g)].map(([, name]) => name);
2405
+ return [
2406
+ ...images.map((image) => ({
2407
+ value: `<${image.name}>`,
2408
+ description: "One of the decision's images",
2409
+ })),
2410
+ ...[...new Set(fields)].map((name) => ({
2411
+ value: name,
2412
+ description: 'The image in this state field',
2413
+ })),
2414
+ ];
2415
+ }
2416
+ /** The buttons on a question's page that move it in the list and remove it. */
2417
+ function QuestionActions({ name, index, count, onMove, onRemove, }) {
2418
+ const up = `Ask ${name} one place earlier. Its answer shows earlier too.`;
2419
+ const down = `Ask ${name} one place later. Its answer shows later too.`;
2420
+ const remove = `Remove ${name}`;
2421
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { content: up, children: _jsx(IconButton, { label: up, variant: "ghost", size: "sm", isDisabled: index === 0, icon: _jsx(Icon, { icon: IconArrowUp, size: "sm" }), onClick: () => {
2422
+ onMove(index - 1);
2423
+ } }) }), _jsx(Tooltip, { content: down, children: _jsx(IconButton, { label: down, variant: "ghost", size: "sm", isDisabled: index === count - 1, icon: _jsx(Icon, { icon: IconArrowDown, size: "sm" }), onClick: () => {
2424
+ onMove(index + 1);
2425
+ } }) }), _jsx(Tooltip, { content: remove, children: _jsx(IconButton, { label: remove, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconTrash, size: "sm" }), onClick: onRemove }) })] }));
2426
+ }
2427
+ /** What a row leads with: a score level's number, or the label of an option or criterion. */
2428
+ function CriterionLabel({ question, row, at, setCriteria }) {
2429
+ const copy = CRITERIA_COPY[question.type];
2430
+ return question.type === 'score' ? (_jsx(StackItem, { size: "static", children: _jsx(Text, { type: "supporting", color: "secondary", hasTabularNumbers: true, children: String(at) }) })) : (_jsx("div", { className: "criterion-label", children: _jsx(TextInput, { label: `${copy.row} ${String(at + 1)} label`, isLabelHidden: true, size: "sm", value: row.label, placeholder: "label", onChange: (label) => {
2431
+ setCriteria(question.criteria.map((r) => (r.key === row.key ? { ...r, label } : r)));
2432
+ } }) }));
2433
+ }
2434
+ function CriterionRow({ question, row, at, setCriteria }) {
2435
+ const copy = CRITERIA_COPY[question.type];
2436
+ return (_jsxs(HStack, { gap: 1, vAlign: "center", children: [_jsx(CriterionLabel, { question: question, row: row, at: at, setCriteria: setCriteria }), _jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: `${copy.row} ${String(at + 1)} description`, isLabelHidden: true, size: "sm", value: row.text, placeholder: question.type === 'score' ? 'What this level means' : 'When to pick it', onChange: (text) => {
2437
+ setCriteria(question.criteria.map((r) => (r.key === row.key ? { ...r, text } : r)));
2438
+ } }) }), _jsx(IconButton, { label: `Remove ${copy.row.toLowerCase()} ${String(at + 1)}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: () => {
2439
+ setCriteria(question.criteria.filter((r) => r.key !== row.key));
2440
+ } })] }));
2441
+ }
2442
+ /** A question's options, levels, or criteria, and the button that adds one. */
2443
+ function CriteriaList({ question, onChange, }) {
2444
+ const keys = useLentKeys();
2445
+ const copy = CRITERIA_COPY[question.type];
2446
+ const setCriteria = (criteria) => {
2447
+ onChange({ criteria });
2448
+ };
2449
+ return (_jsxs(InspectorSection, { title: copy.title, note: copy.note, children: [question.criteria.map((row, at) => (_jsx(CriterionRow, { question: question, row: row, at: at, setCriteria: setCriteria }, row.key))), _jsx(HStack, { children: _jsx(Button, { label: `Add ${copy.row.toLowerCase()}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), isDisabled: question.criteria.length >= (keys?.decision.criteria ?? Infinity), onClick: () => {
2450
+ setCriteria([...question.criteria, ...newCriteria('score').slice(0, 1)]);
2451
+ } }) })] }));
2452
+ }
2453
+ function DecisionContractSection({ decision, set }) {
2454
+ return (_jsx(InspectorSection, { path: "decision.contract", children: _jsx(TextRow, { label: "Contract", path: "decision.contract", field: "contract", value: decision.contract, placeholder: "guardrails.tool_call.v1", onChange: (contract) => {
2455
+ set({ contract });
2456
+ } }) }));
2457
+ }
2458
+ function DecisionStateSection({ decision, set }) {
2459
+ const keys = useLentKeys();
2460
+ return (_jsx(InspectorSection, { children: _jsx(NumberRow, { label: "Max state", path: "inputs.maxStateBytes", units: "bytes", field: "maxStateBytes", value: decision.maxStateBytes, min: 1, max: keys?.decision.stateBytes, isIntegerOnly: true, onChange: (maxStateBytes) => {
2461
+ set({ maxStateBytes });
2462
+ } }) }));
2463
+ }
2464
+ /** The page for the question keyed `open`, or `undefined` when there is none or it is gone. */
2465
+ function openQuestionPage(open, { questions, images }, setQuestions, onBack) {
2466
+ const index = questions.findIndex((question) => question.key === open);
2467
+ const question = questions[index];
2468
+ if (!question)
2469
+ return undefined;
2470
+ return (_jsx(QuestionPage, { question: question, index: index, count: questions.length, images: images, onChange: (change) => {
2471
+ setQuestions((all) => all.map((q) => (q.key === question.key ? { ...q, ...change } : q)));
2472
+ }, onMove: (to) => {
2473
+ setQuestions((all) => {
2474
+ const next = all.filter((q) => q.key !== question.key);
2475
+ next.splice(to, 0, question);
2476
+ return next;
2477
+ });
2478
+ }, onRemove: () => {
2479
+ setQuestions((all) => all.filter((q) => q.key !== question.key));
2480
+ onBack();
2481
+ }, onBack: onBack }));
2482
+ }
2483
+ /**
2484
+ * A decision: the contract it answers to, how much state it takes, and the questions it asks of
2485
+ * it, listed one to a line. A question opens its own page.
2486
+ */
2487
+ function DecisionEditor({ draft, setDraft }) {
2488
+ const { decision } = draft;
2489
+ const set = patch(setDraft, 'decision');
2490
+ const listStatus = useFieldStatus()('questions');
2491
+ const { open, show, frame } = usePageSwap();
2492
+ const setQuestions = (change) => {
2493
+ setDraft((current) => ({
2494
+ ...current,
2495
+ decision: { ...current.decision, questions: change(current.decision.questions) },
2496
+ }));
2497
+ };
2498
+ // A project's files hold the questions outside the profile: the studio shows them and says where.
2499
+ const inFiles = useRowOrigin('decision.questions');
2500
+ const inCode = (rows) => inFiles ? (_jsx(SaidOrigin, { value: inFiles.origin, children: _jsx(SetInCode, { children: rows }) })) : (rows);
2501
+ const { asked, shown } = useContext(QuestionLink);
2502
+ const askedKey = decision.questions.find((question) => question.id === asked)?.key;
2503
+ useEffect(() => {
2504
+ if (asked === undefined)
2505
+ return;
2506
+ shown?.();
2507
+ if (askedKey)
2508
+ show(askedKey);
2509
+ }, [asked, askedKey, shown, show]);
2510
+ const page = openQuestionPage(open, decision, setQuestions, () => {
2511
+ show(null);
2512
+ });
2513
+ if (page)
2514
+ return frame(open ?? '', inCode(page));
2515
+ return frame('list', _jsxs(_Fragment, { children: [_jsx(DecisionContractSection, { decision: decision, set: set }), _jsx(DecisionStateSection, { decision: decision, set: set }), _jsxs(InspectorSection, { title: "Questions", note: sectionNote('decision.questions'), children: [inFiles && !inFiles.said && _jsx(OriginLine, { origin: inFiles.origin, onOpen: inFiles.open }), listStatus && _jsx(Banner, { status: "error", title: listStatus.message }), inCode(_jsxs(_Fragment, { children: [decision.questions.length > 0 && (_jsx(QuestionList, { questions: decision.questions, onOpen: show })), _jsx(AddQuestionButton, { draft: draft, setDraft: setDraft, onOpen: show })] }))] }), _jsx(DecisionImagesSection, { draft: draft, setDraft: setDraft })] }));
2516
+ }
2517
+ /** One of a decision's pictures: its thumbnail, the name its questions write it by, and a remove button. */
2518
+ function DecisionImageRow({ image, index, onRename, onRemove, }) {
2519
+ const status = useFieldStatus()('images', index);
2520
+ const label = `Image ${String(index + 1)}`;
2521
+ const reference = `<${image.name}>`;
2522
+ return (_jsxs(InspectorRow, { label: label, path: "decision.images.name", hasIssue: status !== undefined, children: [_jsx(Thumbnail, { src: `data:${image.mimeType};base64,${image.data}`, label: `Questions write it as ${reference}` }), _jsx(StackItem, { size: "fill", children: _jsx(TextInput, { label: `${label} name`, isLabelHidden: true, size: "sm", status: status, value: image.name, onChange: onRename }) }), _jsx(Tooltip, { content: `Remove ${reference}. A question that names it then has no image to read.`, children: _jsx(IconButton, { label: `Remove ${reference}`, variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconX, size: "sm" }), onClick: onRemove }) })] }));
2523
+ }
2524
+ /** The pictures every decision sends, each under the name its questions write, and a drop to add more. */
2525
+ function DecisionImagesSection({ draft, setDraft }) {
2526
+ const keys = useLentKeys();
2527
+ const { reading, changeAction } = useFilesRead(async (picked) => {
2528
+ const read = await Promise.all(picked.map(async (file) => ({ file: file.name, image: await fileData(file) })));
2529
+ setDraft((current) => {
2530
+ let next = current;
2531
+ for (const { file, image } of read) {
2532
+ const added = newDecisionImage(next, file, image);
2533
+ next = {
2534
+ ...next,
2535
+ decision: { ...next.decision, images: [...next.decision.images, added] },
2536
+ };
2537
+ }
2538
+ return next;
2539
+ });
2540
+ });
2541
+ const listStatus = useFieldStatus()('images');
2542
+ const { images } = draft.decision;
2543
+ const setImages = (change) => {
2544
+ setDraft((current) => ({
2545
+ ...current,
2546
+ decision: { ...current.decision, images: change(current.decision.images) },
2547
+ }));
2548
+ };
2549
+ const most = keys?.decision.images ?? Infinity;
2550
+ return (_jsxs(InspectorSection, { title: "Images", path: "decision.images", children: [listStatus && _jsx(Banner, { status: "error", title: listStatus.message }), images.map((image, index) => (_jsx(DecisionImageRow, { image: image, index: index, onRename: (name) => {
2551
+ setImages((all) => all.map((each) => (each.key === image.key ? { ...each, name } : each)));
2552
+ }, onRemove: () => {
2553
+ setImages((all) => all.filter((each) => each.key !== image.key));
2554
+ } }, image.key))), _jsx(FileInput, { label: "Add images", isLabelHidden: true, mode: "dropzone", accept: "image/png,image/jpeg", isMultiple: true, isDisabled: images.length >= most, maxSize: keys?.decision.imageBytes, isLoading: reading, placeholder: "Drop a PNG or JPEG, or choose one. Its file name becomes the name a question writes.", value: null, changeAction: changeAction, onChange: () => undefined })] }));
2555
+ }
2556
+ /** Adds a question and opens its page, up to the most a decision takes on lent keys. */
2557
+ function AddQuestionButton({ draft, setDraft, onOpen, }) {
2558
+ const keys = useLentKeys();
2559
+ const count = draft.decision.questions.length;
2560
+ return (_jsx(HStack, { children: _jsx(Button, { label: "Add question", variant: "ghost", size: "sm", icon: _jsx(Icon, { icon: IconPlus, size: "sm" }), isDisabled: count >= (keys?.decision.questions ?? Infinity), onClick: () => {
2561
+ const question = newDecisionQuestion(draft);
2562
+ setDraft((current) => ({
2563
+ ...current,
2564
+ decision: {
2565
+ ...current.decision,
2566
+ questions: [...current.decision.questions, question],
2567
+ },
2568
+ }));
2569
+ onOpen(question.key);
2570
+ } }) }));
2571
+ }
2572
+ /** The editor for a node's facet: one per section of the profile, model, and tool. */
2573
+ function facetEditor(ref, draft, setDraft, onSelect) {
2574
+ const props = { draft, setDraft };
2575
+ switch (ref.facet) {
2576
+ case 'identity':
2577
+ return _jsx(IdentityEditor, { ...props });
2578
+ case 'models':
2579
+ return _jsx(ModelsEditor, { ...props, onSelect: onSelect });
2580
+ case 'modelBinding':
2581
+ return draft.identity.profileType === 'decision' ? (_jsx(DecisionModelEditor, { ...props, bindingKey: ref.key })) : (_jsx(ModelBindingEditor, { ...props, bindingKey: ref.key, onSelect: onSelect }));
2582
+ case 'tools':
2583
+ return _jsx(ToolsEditor, { ...props, onSelect: onSelect });
2584
+ case 'toolSpec':
2585
+ return _jsx(ToolSpecEditor, { ...props, toolKey: ref.key, onSelect: onSelect });
2586
+ case 'inputs':
2587
+ return _jsx(InputsEditor, { ...props });
2588
+ case 'image':
2589
+ return _jsx(ImageEditor, { ...props });
2590
+ case 'speech':
2591
+ return _jsx(SpeechEditor, { ...props });
2592
+ case 'live':
2593
+ return _jsx(LiveEditor, { ...props });
2594
+ case 'outputs':
2595
+ return _jsx(OutputsEditor, { ...props });
2596
+ case 'turnBehaviour':
2597
+ return _jsx(TurnBehaviourEditor, { ...props });
2598
+ case 'guardrails':
2599
+ return _jsx(GuardrailsEditor, { ...props });
2600
+ case 'observability':
2601
+ return _jsx(ObservabilityEditor, { ...props });
2602
+ case 'labels':
2603
+ return _jsx(LabelsEditor, { ...props, onSelect: onSelect });
2604
+ case 'decision':
2605
+ return _jsx(DecisionEditor, { ...props });
2606
+ }
2607
+ }
2608
+ /** A node's issues that name no field of it, over its rows. */
2609
+ export function IssueBanners({ issues }) {
2610
+ const banners = issues.filter((issue) => issue.field === undefined);
2611
+ if (banners.length === 0)
2612
+ return null;
2613
+ return (_jsx(Section, { variant: "transparent", padding: 3, children: _jsx(VStack, { gap: 2, children: banners.map((issue) => (_jsx(Banner, { status: "error", title: issue.message }, issue.message))) }) }));
2614
+ }
2615
+ /**
2616
+ * The editor for the tree node `selectedId`. The compile's issues for that node show on the rows
2617
+ * of the fields they name; the rest show above it.
2618
+ * Edits go straight to the draft; the page compiles it.
2619
+ */
2620
+ export function ProfileEditor({ draft, setDraft, selectedId, onSelect, issues, }) {
2621
+ // One place in the project's files sets the whole node: the page says so over it, and no row takes an edit.
2622
+ const setInCode = useContext(RowOrigins)?.scope.section !== undefined;
2623
+ const ref = studioNodeRef(draft, selectedId);
2624
+ if (!ref)
2625
+ return null;
2626
+ const nodeIssues = issues.filter((issue) => issue.nodeId === selectedId);
2627
+ const editor = facetEditor(ref, draft, setDraft, onSelect);
2628
+ return (_jsx(NodeIssues, { value: nodeIssues, children: _jsxs(VStack, { children: [_jsx(IssueBanners, { issues: nodeIssues }), setInCode ? _jsx(SetInCode, { children: editor }) : editor] }) }));
2629
+ }
2630
+ /** Local servers answer local pages only; a hosted page needs its origin allowed. */
2631
+ const isLocalPage = () => typeof window !== 'undefined' &&
2632
+ /^(localhost|127\.0\.0\.1|\[::1\])$/.test(globalThis.location.hostname);
2633
+ /** Allows the page the builder is on, wherever it's hosted. */
2634
+ const ollamaOriginsCommand = () => `launchctl setenv OLLAMA_ORIGINS "${globalThis.location.origin}"`;
2635
+ /** Why a hosted page can't list local models, and the one command that fixes it. */
2636
+ function LocalOriginHelp() {
2637
+ return (_jsx(Banner, { status: "warning", title: "Your local server must allow this site", description: _jsxs(VStack, { gap: 2, children: [_jsx(Text, { type: "supporting", children: "Ollama only answers pages on your own machine. Run this, then quit and reopen Ollama. LM Studio: turn on CORS in its server settings." }), _jsx(CodeBlock, { code: ollamaOriginsCommand(), language: "bash", hasLanguageLabel: false, isWrapped: true, size: "sm" })] }) }));
2638
+ }