corporateai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (704) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +3 -0
  3. package/.github/workflows/publish-cli.yml +49 -0
  4. package/.mailmap +1 -0
  5. package/AGENTS.md +148 -0
  6. package/CONTRIBUTING.md +75 -0
  7. package/Dockerfile +59 -0
  8. package/Dockerfile.onboard-smoke +42 -0
  9. package/LICENSE +21 -0
  10. package/README.md +93 -0
  11. package/cli/esbuild.config.mjs +11 -0
  12. package/cli/package.json +24 -0
  13. package/cli/scripts/build-cli.mjs +5 -0
  14. package/cli/src/index.ts +27 -0
  15. package/docker-compose.quickstart.yml +18 -0
  16. package/docker-compose.untrusted-review.yml +33 -0
  17. package/docker-compose.yml +38 -0
  18. package/package.json +56 -0
  19. package/patches/embedded-postgres@18.1.0-beta.16.patch +0 -0
  20. package/pnpm-workspace.yaml +4 -0
  21. package/releases/.gitkeep +0 -0
  22. package/releases/v0.0.1.md +36 -0
  23. package/releases/v0.2.7.md +15 -0
  24. package/releases/v0.3.0.md +54 -0
  25. package/releases/v0.3.1.md +55 -0
  26. package/releases/v2026.318.0.md +66 -0
  27. package/releases/v2026.325.0.md +78 -0
  28. package/report/2026-03-13-08-46-token-optimization-implementation.md +48 -0
  29. package/scripts/backup-db.sh +17 -0
  30. package/scripts/build-npm.sh +80 -0
  31. package/scripts/check-forbidden-tokens.mjs +115 -0
  32. package/scripts/clean-onboard-git.sh +14 -0
  33. package/scripts/clean-onboard-npm.sh +13 -0
  34. package/scripts/clean-onboard-ref.sh +86 -0
  35. package/scripts/create-github-release.sh +99 -0
  36. package/scripts/dev-runner-paths.mjs +38 -0
  37. package/scripts/dev-runner.mjs +606 -0
  38. package/scripts/docker-onboard-smoke.sh +306 -0
  39. package/scripts/ensure-plugin-build-deps.mjs +47 -0
  40. package/scripts/generate-company-assets.ts +365 -0
  41. package/scripts/generate-npm-package-json.mjs +113 -0
  42. package/scripts/generate-org-chart-images.ts +694 -0
  43. package/scripts/generate-org-chart-satori-comparison.ts +225 -0
  44. package/scripts/generate-ui-package-json.mjs +31 -0
  45. package/scripts/kill-dev.sh +71 -0
  46. package/scripts/migrate-inline-env-secrets.ts +126 -0
  47. package/scripts/prepare-server-ui-dist.sh +22 -0
  48. package/scripts/provision-worktree.sh +333 -0
  49. package/scripts/release-lib.sh +306 -0
  50. package/scripts/release-package-map.mjs +169 -0
  51. package/scripts/release.sh +312 -0
  52. package/scripts/rollback-latest.sh +111 -0
  53. package/scripts/smoke/openclaw-docker-ui.sh +329 -0
  54. package/scripts/smoke/openclaw-gateway-e2e.sh +954 -0
  55. package/scripts/smoke/openclaw-join.sh +295 -0
  56. package/scripts/smoke/openclaw-sse-standalone.sh +146 -0
  57. package/scripts/workspace-compat.mjs +60 -0
  58. package/server/CHANGELOG.md +130 -0
  59. package/server/package.json +96 -0
  60. package/server/scripts/copy-onboarding-assets.mjs +10 -0
  61. package/server/scripts/dev-watch.ts +33 -0
  62. package/server/src/__tests__/activity-routes.test.ts +70 -0
  63. package/server/src/__tests__/adapter-models.test.ts +105 -0
  64. package/server/src/__tests__/adapter-session-codecs.test.ts +194 -0
  65. package/server/src/__tests__/agent-auth-jwt.test.ts +79 -0
  66. package/server/src/__tests__/agent-instructions-routes.test.ts +318 -0
  67. package/server/src/__tests__/agent-instructions-service.test.ts +361 -0
  68. package/server/src/__tests__/agent-permissions-routes.test.ts +275 -0
  69. package/server/src/__tests__/agent-shortname-collision.test.ts +69 -0
  70. package/server/src/__tests__/agent-skill-contract.test.ts +50 -0
  71. package/server/src/__tests__/agent-skills-routes.test.ts +462 -0
  72. package/server/src/__tests__/app-hmr-port.test.ts +19 -0
  73. package/server/src/__tests__/approval-routes-idempotency.test.ts +110 -0
  74. package/server/src/__tests__/approvals-service.test.ts +107 -0
  75. package/server/src/__tests__/assets.test.ts +250 -0
  76. package/server/src/__tests__/attachment-types.test.ts +97 -0
  77. package/server/src/__tests__/board-mutation-guard.test.ts +105 -0
  78. package/server/src/__tests__/budgets-service.test.ts +311 -0
  79. package/server/src/__tests__/claude-local-adapter-environment.test.ts +92 -0
  80. package/server/src/__tests__/claude-local-adapter.test.ts +31 -0
  81. package/server/src/__tests__/claude-local-skill-sync.test.ts +111 -0
  82. package/server/src/__tests__/cli-auth-routes.test.ts +230 -0
  83. package/server/src/__tests__/codex-local-adapter-environment.test.ts +143 -0
  84. package/server/src/__tests__/codex-local-adapter.test.ts +253 -0
  85. package/server/src/__tests__/codex-local-execute.test.ts +391 -0
  86. package/server/src/__tests__/codex-local-skill-injection.test.ts +175 -0
  87. package/server/src/__tests__/codex-local-skill-sync.test.ts +123 -0
  88. package/server/src/__tests__/companies-route-path-guard.test.ts +56 -0
  89. package/server/src/__tests__/company-branding-route.test.ts +196 -0
  90. package/server/src/__tests__/company-portability-routes.test.ts +175 -0
  91. package/server/src/__tests__/company-portability.test.ts +2186 -0
  92. package/server/src/__tests__/company-skills-routes.test.ts +113 -0
  93. package/server/src/__tests__/company-skills.test.ts +229 -0
  94. package/server/src/__tests__/costs-service.test.ts +226 -0
  95. package/server/src/__tests__/cursor-local-adapter-environment.test.ts +196 -0
  96. package/server/src/__tests__/cursor-local-adapter.test.ts +406 -0
  97. package/server/src/__tests__/cursor-local-execute.test.ts +263 -0
  98. package/server/src/__tests__/cursor-local-skill-injection.test.ts +104 -0
  99. package/server/src/__tests__/cursor-local-skill-sync.test.ts +145 -0
  100. package/server/src/__tests__/dev-runner-paths.test.ts +25 -0
  101. package/server/src/__tests__/dev-server-status.test.ts +66 -0
  102. package/server/src/__tests__/dev-watch-ignore.test.ts +42 -0
  103. package/server/src/__tests__/documents.test.ts +29 -0
  104. package/server/src/__tests__/error-handler.test.ts +53 -0
  105. package/server/src/__tests__/execution-workspace-policy.test.ts +170 -0
  106. package/server/src/__tests__/forbidden-tokens.test.ts +77 -0
  107. package/server/src/__tests__/gemini-local-adapter-environment.test.ts +135 -0
  108. package/server/src/__tests__/gemini-local-adapter.test.ts +190 -0
  109. package/server/src/__tests__/gemini-local-execute.test.ts +172 -0
  110. package/server/src/__tests__/gemini-local-skill-sync.test.ts +90 -0
  111. package/server/src/__tests__/health.test.ts +16 -0
  112. package/server/src/__tests__/heartbeat-process-recovery.test.ts +256 -0
  113. package/server/src/__tests__/heartbeat-run-summary.test.ts +33 -0
  114. package/server/src/__tests__/heartbeat-workspace-session.test.ts +334 -0
  115. package/server/src/__tests__/helpers/embedded-postgres.ts +7 -0
  116. package/server/src/__tests__/hire-hook.test.ts +181 -0
  117. package/server/src/__tests__/instance-settings-routes.test.ts +156 -0
  118. package/server/src/__tests__/invite-accept-gateway-defaults.test.ts +119 -0
  119. package/server/src/__tests__/invite-accept-replay.test.ts +92 -0
  120. package/server/src/__tests__/invite-expiry.test.ts +10 -0
  121. package/server/src/__tests__/invite-join-grants.test.ts +57 -0
  122. package/server/src/__tests__/invite-join-manager.test.ts +33 -0
  123. package/server/src/__tests__/invite-onboarding-text.test.ts +116 -0
  124. package/server/src/__tests__/issue-comment-reopen-routes.test.ts +146 -0
  125. package/server/src/__tests__/issue-goal-fallback.test.ts +99 -0
  126. package/server/src/__tests__/issues-checkout-wakeup.test.ts +48 -0
  127. package/server/src/__tests__/issues-goal-context-routes.test.ts +187 -0
  128. package/server/src/__tests__/issues-service.test.ts +317 -0
  129. package/server/src/__tests__/issues-user-context.test.ts +113 -0
  130. package/server/src/__tests__/log-redaction.test.ts +74 -0
  131. package/server/src/__tests__/monthly-spend-service.test.ts +90 -0
  132. package/server/src/__tests__/normalize-agent-mention-token.test.ts +41 -0
  133. package/server/src/__tests__/openclaw-gateway-adapter.test.ts +626 -0
  134. package/server/src/__tests__/openclaw-invite-prompt-route.test.ts +192 -0
  135. package/server/src/__tests__/opencode-local-adapter-environment.test.ts +97 -0
  136. package/server/src/__tests__/opencode-local-adapter.test.ts +226 -0
  137. package/server/src/__tests__/opencode-local-skill-sync.test.ts +91 -0
  138. package/server/src/__tests__/paperclip-env.test.ts +58 -0
  139. package/server/src/__tests__/paperclip-skill-utils.test.ts +63 -0
  140. package/server/src/__tests__/pi-local-adapter-environment.test.ts +102 -0
  141. package/server/src/__tests__/pi-local-skill-sync.test.ts +95 -0
  142. package/server/src/__tests__/plugin-dev-watcher.test.ts +68 -0
  143. package/server/src/__tests__/plugin-worker-manager.test.ts +43 -0
  144. package/server/src/__tests__/private-hostname-guard.test.ts +56 -0
  145. package/server/src/__tests__/project-shortname-resolution.test.ts +45 -0
  146. package/server/src/__tests__/quota-windows-service.test.ts +56 -0
  147. package/server/src/__tests__/quota-windows.test.ts +1109 -0
  148. package/server/src/__tests__/redaction.test.ts +66 -0
  149. package/server/src/__tests__/routines-e2e.test.ts +276 -0
  150. package/server/src/__tests__/routines-routes.test.ts +271 -0
  151. package/server/src/__tests__/routines-service.test.ts +424 -0
  152. package/server/src/__tests__/storage-local-provider.test.ts +78 -0
  153. package/server/src/__tests__/ui-branding.test.ts +82 -0
  154. package/server/src/__tests__/work-products.test.ts +95 -0
  155. package/server/src/__tests__/workspace-runtime.test.ts +1131 -0
  156. package/server/src/__tests__/worktree-config.test.ts +426 -0
  157. package/server/src/adapters/codex-models.ts +105 -0
  158. package/server/src/adapters/cursor-models.ts +171 -0
  159. package/server/src/adapters/http/execute.ts +42 -0
  160. package/server/src/adapters/http/index.ts +21 -0
  161. package/server/src/adapters/http/test.ts +116 -0
  162. package/server/src/adapters/index.ts +18 -0
  163. package/server/src/adapters/process/execute.ts +77 -0
  164. package/server/src/adapters/process/index.ts +24 -0
  165. package/server/src/adapters/process/test.ts +89 -0
  166. package/server/src/adapters/registry.ts +225 -0
  167. package/server/src/adapters/server-utils-compat.ts +57 -0
  168. package/server/src/adapters/types.ts +30 -0
  169. package/server/src/adapters/utils.ts +48 -0
  170. package/server/src/agent-auth-jwt.ts +141 -0
  171. package/server/src/app.ts +321 -0
  172. package/server/src/attachment-types.ts +74 -0
  173. package/server/src/auth/better-auth.ts +148 -0
  174. package/server/src/board-claim.ts +150 -0
  175. package/server/src/config-file.ts +17 -0
  176. package/server/src/config.ts +260 -0
  177. package/server/src/dev-server-status.ts +103 -0
  178. package/server/src/dev-watch-ignore.ts +36 -0
  179. package/server/src/errors.ts +34 -0
  180. package/server/src/home-paths.ts +95 -0
  181. package/server/src/index.ts +799 -0
  182. package/server/src/log-redaction.ts +146 -0
  183. package/server/src/middleware/auth.ts +178 -0
  184. package/server/src/middleware/board-mutation-guard.ts +66 -0
  185. package/server/src/middleware/error-handler.ts +71 -0
  186. package/server/src/middleware/index.ts +3 -0
  187. package/server/src/middleware/logger.ts +90 -0
  188. package/server/src/middleware/private-hostname-guard.ts +92 -0
  189. package/server/src/middleware/validate.ts +9 -0
  190. package/server/src/onboarding-assets/ceo/AGENTS.md +54 -0
  191. package/server/src/onboarding-assets/ceo/HEARTBEAT.md +72 -0
  192. package/server/src/onboarding-assets/ceo/SOUL.md +33 -0
  193. package/server/src/onboarding-assets/ceo/TOOLS.md +3 -0
  194. package/server/src/onboarding-assets/default/AGENTS.md +3 -0
  195. package/server/src/paths.ts +34 -0
  196. package/server/src/realtime/live-events-ws.ts +274 -0
  197. package/server/src/redaction.ts +59 -0
  198. package/server/src/routes/access.ts +2888 -0
  199. package/server/src/routes/activity.ts +89 -0
  200. package/server/src/routes/agents.ts +2313 -0
  201. package/server/src/routes/approvals.ts +346 -0
  202. package/server/src/routes/assets.ts +341 -0
  203. package/server/src/routes/authz.ts +52 -0
  204. package/server/src/routes/companies.ts +343 -0
  205. package/server/src/routes/company-skills.ts +300 -0
  206. package/server/src/routes/costs.ts +335 -0
  207. package/server/src/routes/dashboard.ts +19 -0
  208. package/server/src/routes/execution-workspaces.ts +182 -0
  209. package/server/src/routes/goals.ts +107 -0
  210. package/server/src/routes/health.ts +94 -0
  211. package/server/src/routes/index.ts +17 -0
  212. package/server/src/routes/instance-settings.ts +95 -0
  213. package/server/src/routes/issues-checkout-wakeup.ts +14 -0
  214. package/server/src/routes/issues.ts +1680 -0
  215. package/server/src/routes/llms.ts +86 -0
  216. package/server/src/routes/org-chart-svg.ts +777 -0
  217. package/server/src/routes/plugin-ui-static.ts +497 -0
  218. package/server/src/routes/plugins.ts +2220 -0
  219. package/server/src/routes/projects.ts +295 -0
  220. package/server/src/routes/routines.ts +300 -0
  221. package/server/src/routes/secrets.ts +166 -0
  222. package/server/src/routes/sidebar-badges.ts +52 -0
  223. package/server/src/secrets/external-stub-providers.ts +32 -0
  224. package/server/src/secrets/local-encrypted-provider.ts +135 -0
  225. package/server/src/secrets/provider-registry.ts +31 -0
  226. package/server/src/secrets/types.ts +23 -0
  227. package/server/src/services/access.ts +381 -0
  228. package/server/src/services/activity-log.ts +95 -0
  229. package/server/src/services/activity.ts +164 -0
  230. package/server/src/services/agent-instructions.ts +735 -0
  231. package/server/src/services/agent-permissions.ts +27 -0
  232. package/server/src/services/agents.ts +694 -0
  233. package/server/src/services/approvals.ts +273 -0
  234. package/server/src/services/assets.ts +23 -0
  235. package/server/src/services/board-auth.ts +355 -0
  236. package/server/src/services/budgets.ts +959 -0
  237. package/server/src/services/companies.ts +313 -0
  238. package/server/src/services/company-export-readme.ts +173 -0
  239. package/server/src/services/company-portability.ts +4263 -0
  240. package/server/src/services/company-skills.ts +2356 -0
  241. package/server/src/services/costs.ts +365 -0
  242. package/server/src/services/cron.ts +373 -0
  243. package/server/src/services/dashboard.ts +110 -0
  244. package/server/src/services/default-agent-instructions.ts +27 -0
  245. package/server/src/services/documents.ts +434 -0
  246. package/server/src/services/execution-workspace-policy.ts +210 -0
  247. package/server/src/services/execution-workspaces.ts +100 -0
  248. package/server/src/services/finance.ts +135 -0
  249. package/server/src/services/goals.ts +81 -0
  250. package/server/src/services/heartbeat-run-summary.ts +35 -0
  251. package/server/src/services/heartbeat.ts +3863 -0
  252. package/server/src/services/hire-hook.ts +114 -0
  253. package/server/src/services/index.ts +32 -0
  254. package/server/src/services/instance-settings.ts +138 -0
  255. package/server/src/services/issue-approvals.ts +175 -0
  256. package/server/src/services/issue-assignment-wakeup.ts +48 -0
  257. package/server/src/services/issue-goal-fallback.ts +56 -0
  258. package/server/src/services/issues.ts +1828 -0
  259. package/server/src/services/live-events.ts +55 -0
  260. package/server/src/services/plugin-capability-validator.ts +450 -0
  261. package/server/src/services/plugin-config-validator.ts +55 -0
  262. package/server/src/services/plugin-dev-watcher.ts +339 -0
  263. package/server/src/services/plugin-event-bus.ts +413 -0
  264. package/server/src/services/plugin-host-service-cleanup.ts +59 -0
  265. package/server/src/services/plugin-host-services.ts +1132 -0
  266. package/server/src/services/plugin-job-coordinator.ts +261 -0
  267. package/server/src/services/plugin-job-scheduler.ts +753 -0
  268. package/server/src/services/plugin-job-store.ts +466 -0
  269. package/server/src/services/plugin-lifecycle.ts +822 -0
  270. package/server/src/services/plugin-loader.ts +1955 -0
  271. package/server/src/services/plugin-log-retention.ts +87 -0
  272. package/server/src/services/plugin-manifest-validator.ts +164 -0
  273. package/server/src/services/plugin-registry.ts +683 -0
  274. package/server/src/services/plugin-runtime-sandbox.ts +222 -0
  275. package/server/src/services/plugin-secrets-handler.ts +355 -0
  276. package/server/src/services/plugin-state-store.ts +238 -0
  277. package/server/src/services/plugin-stream-bus.ts +81 -0
  278. package/server/src/services/plugin-tool-dispatcher.ts +449 -0
  279. package/server/src/services/plugin-tool-registry.ts +450 -0
  280. package/server/src/services/plugin-worker-manager.ts +1343 -0
  281. package/server/src/services/projects.ts +860 -0
  282. package/server/src/services/quota-windows.ts +65 -0
  283. package/server/src/services/routines.ts +1269 -0
  284. package/server/src/services/run-log-store.ts +156 -0
  285. package/server/src/services/secrets.ts +370 -0
  286. package/server/src/services/sidebar-badges.ts +56 -0
  287. package/server/src/services/work-products.ts +124 -0
  288. package/server/src/services/workspace-operation-log-store.ts +156 -0
  289. package/server/src/services/workspace-operations.ts +262 -0
  290. package/server/src/services/workspace-runtime.ts +1565 -0
  291. package/server/src/startup-banner.ts +176 -0
  292. package/server/src/storage/index.ts +35 -0
  293. package/server/src/storage/local-disk-provider.ts +89 -0
  294. package/server/src/storage/provider-registry.ts +18 -0
  295. package/server/src/storage/s3-provider.ts +153 -0
  296. package/server/src/storage/service.ts +131 -0
  297. package/server/src/storage/types.ts +63 -0
  298. package/server/src/ui-branding.ts +217 -0
  299. package/server/src/version.ts +10 -0
  300. package/server/src/worktree-config.ts +468 -0
  301. package/server/tsconfig.json +9 -0
  302. package/server/vitest.config.ts +7 -0
  303. package/skills/paperclip/SKILL.md +365 -0
  304. package/skills/paperclip/references/api-reference.md +647 -0
  305. package/skills/paperclip/references/company-skills.md +193 -0
  306. package/skills/paperclip-create-agent/SKILL.md +142 -0
  307. package/skills/paperclip-create-agent/references/api-reference.md +105 -0
  308. package/skills/paperclip-create-plugin/SKILL.md +102 -0
  309. package/skills/para-memory-files/SKILL.md +104 -0
  310. package/skills/para-memory-files/references/schemas.md +35 -0
  311. package/tests/e2e/onboarding.spec.ts +142 -0
  312. package/tests/e2e/playwright.config.ts +35 -0
  313. package/tests/release-smoke/docker-auth-onboarding.spec.ts +141 -0
  314. package/tests/release-smoke/playwright.config.ts +28 -0
  315. package/tsconfig.base.json +18 -0
  316. package/tsconfig.json +18 -0
  317. package/ui/README.md +12 -0
  318. package/ui/components.json +21 -0
  319. package/ui/index.html +47 -0
  320. package/ui/package.json +73 -0
  321. package/ui/public/android-chrome-192x192.png +0 -0
  322. package/ui/public/android-chrome-512x512.png +0 -0
  323. package/ui/public/apple-touch-icon.png +0 -0
  324. package/ui/public/brands/opencode-logo-dark-square.svg +18 -0
  325. package/ui/public/brands/opencode-logo-light-square.svg +18 -0
  326. package/ui/public/favicon-16x16.png +0 -0
  327. package/ui/public/favicon-32x32.png +0 -0
  328. package/ui/public/favicon.ico +0 -0
  329. package/ui/public/favicon.svg +9 -0
  330. package/ui/public/site.webmanifest +30 -0
  331. package/ui/public/sprites/1-D-1.png +0 -0
  332. package/ui/public/sprites/1-D-2.png +0 -0
  333. package/ui/public/sprites/1-D-3.png +0 -0
  334. package/ui/public/sprites/1-L-1.png +0 -0
  335. package/ui/public/sprites/1-R-1.png +0 -0
  336. package/ui/public/sprites/10-D-1.png +0 -0
  337. package/ui/public/sprites/10-D-2.png +0 -0
  338. package/ui/public/sprites/10-D-3.png +0 -0
  339. package/ui/public/sprites/10-L-1.png +0 -0
  340. package/ui/public/sprites/10-R-1.png +0 -0
  341. package/ui/public/sprites/11-D-1.png +0 -0
  342. package/ui/public/sprites/11-D-2.png +0 -0
  343. package/ui/public/sprites/11-D-3.png +0 -0
  344. package/ui/public/sprites/11-L-1.png +0 -0
  345. package/ui/public/sprites/11-R-1.png +0 -0
  346. package/ui/public/sprites/12-D-1.png +0 -0
  347. package/ui/public/sprites/12-D-2.png +0 -0
  348. package/ui/public/sprites/12-D-3.png +0 -0
  349. package/ui/public/sprites/12-L-1.png +0 -0
  350. package/ui/public/sprites/12-R-1.png +0 -0
  351. package/ui/public/sprites/13-D-1.png +0 -0
  352. package/ui/public/sprites/13-D-2.png +0 -0
  353. package/ui/public/sprites/13-D-3.png +0 -0
  354. package/ui/public/sprites/13-L-1.png +0 -0
  355. package/ui/public/sprites/13-R-1.png +0 -0
  356. package/ui/public/sprites/14-D-1.png +0 -0
  357. package/ui/public/sprites/14-D-2.png +0 -0
  358. package/ui/public/sprites/14-D-3.png +0 -0
  359. package/ui/public/sprites/14-L-1.png +0 -0
  360. package/ui/public/sprites/14-R-1.png +0 -0
  361. package/ui/public/sprites/2-D-1.png +0 -0
  362. package/ui/public/sprites/2-D-2.png +0 -0
  363. package/ui/public/sprites/2-D-3.png +0 -0
  364. package/ui/public/sprites/2-L-1.png +0 -0
  365. package/ui/public/sprites/2-R-1.png +0 -0
  366. package/ui/public/sprites/3-D-1.png +0 -0
  367. package/ui/public/sprites/3-D-2.png +0 -0
  368. package/ui/public/sprites/3-D-3.png +0 -0
  369. package/ui/public/sprites/3-L-1.png +0 -0
  370. package/ui/public/sprites/3-R-1.png +0 -0
  371. package/ui/public/sprites/4-D-1.png +0 -0
  372. package/ui/public/sprites/4-D-2.png +0 -0
  373. package/ui/public/sprites/4-D-3.png +0 -0
  374. package/ui/public/sprites/4-L-1.png +0 -0
  375. package/ui/public/sprites/4-R-1.png +0 -0
  376. package/ui/public/sprites/5-D-1.png +0 -0
  377. package/ui/public/sprites/5-D-2.png +0 -0
  378. package/ui/public/sprites/5-D-3.png +0 -0
  379. package/ui/public/sprites/5-L-1.png +0 -0
  380. package/ui/public/sprites/5-R-1.png +0 -0
  381. package/ui/public/sprites/6-D-1.png +0 -0
  382. package/ui/public/sprites/6-D-2.png +0 -0
  383. package/ui/public/sprites/6-D-3.png +0 -0
  384. package/ui/public/sprites/6-L-1.png +0 -0
  385. package/ui/public/sprites/6-R-1.png +0 -0
  386. package/ui/public/sprites/7-D-1.png +0 -0
  387. package/ui/public/sprites/7-D-2.png +0 -0
  388. package/ui/public/sprites/7-D-3.png +0 -0
  389. package/ui/public/sprites/7-L-1.png +0 -0
  390. package/ui/public/sprites/7-R-1.png +0 -0
  391. package/ui/public/sprites/8-D-1.png +0 -0
  392. package/ui/public/sprites/8-D-2.png +0 -0
  393. package/ui/public/sprites/8-D-3.png +0 -0
  394. package/ui/public/sprites/8-L-1.png +0 -0
  395. package/ui/public/sprites/8-R-1.png +0 -0
  396. package/ui/public/sprites/9-D-1.png +0 -0
  397. package/ui/public/sprites/9-D-2.png +0 -0
  398. package/ui/public/sprites/9-D-3.png +0 -0
  399. package/ui/public/sprites/9-L-1.png +0 -0
  400. package/ui/public/sprites/9-R-1.png +0 -0
  401. package/ui/public/sprites/ceo-lobster.png +0 -0
  402. package/ui/public/sw.js +42 -0
  403. package/ui/public/worktree-favicon-16x16.png +0 -0
  404. package/ui/public/worktree-favicon-32x32.png +0 -0
  405. package/ui/public/worktree-favicon.ico +0 -0
  406. package/ui/public/worktree-favicon.svg +9 -0
  407. package/ui/src/App.tsx +354 -0
  408. package/ui/src/adapters/claude-local/config-fields.tsx +138 -0
  409. package/ui/src/adapters/claude-local/index.ts +13 -0
  410. package/ui/src/adapters/codex-local/config-fields.tsx +104 -0
  411. package/ui/src/adapters/codex-local/index.ts +13 -0
  412. package/ui/src/adapters/cursor/config-fields.tsx +49 -0
  413. package/ui/src/adapters/cursor/index.ts +13 -0
  414. package/ui/src/adapters/gemini-local/config-fields.tsx +51 -0
  415. package/ui/src/adapters/gemini-local/index.ts +13 -0
  416. package/ui/src/adapters/http/build-config.ts +9 -0
  417. package/ui/src/adapters/http/config-fields.tsx +38 -0
  418. package/ui/src/adapters/http/index.ts +12 -0
  419. package/ui/src/adapters/http/parse-stdout.ts +5 -0
  420. package/ui/src/adapters/index.ts +9 -0
  421. package/ui/src/adapters/local-workspace-runtime-fields.tsx +5 -0
  422. package/ui/src/adapters/openclaw-gateway/config-fields.tsx +237 -0
  423. package/ui/src/adapters/openclaw-gateway/index.ts +13 -0
  424. package/ui/src/adapters/opencode-local/config-fields.tsx +72 -0
  425. package/ui/src/adapters/opencode-local/index.ts +13 -0
  426. package/ui/src/adapters/pi-local/config-fields.tsx +49 -0
  427. package/ui/src/adapters/pi-local/index.ts +13 -0
  428. package/ui/src/adapters/process/build-config.ts +18 -0
  429. package/ui/src/adapters/process/config-fields.tsx +77 -0
  430. package/ui/src/adapters/process/index.ts +12 -0
  431. package/ui/src/adapters/process/parse-stdout.ts +5 -0
  432. package/ui/src/adapters/registry.ts +34 -0
  433. package/ui/src/adapters/runtime-json-fields.tsx +122 -0
  434. package/ui/src/adapters/transcript.test.ts +30 -0
  435. package/ui/src/adapters/transcript.ts +62 -0
  436. package/ui/src/adapters/types.ts +34 -0
  437. package/ui/src/api/access.ts +160 -0
  438. package/ui/src/api/activity.ts +37 -0
  439. package/ui/src/api/agents.ts +194 -0
  440. package/ui/src/api/approvals.ts +25 -0
  441. package/ui/src/api/assets.ts +30 -0
  442. package/ui/src/api/auth.ts +74 -0
  443. package/ui/src/api/budgets.ts +21 -0
  444. package/ui/src/api/client.ts +50 -0
  445. package/ui/src/api/companies.ts +59 -0
  446. package/ui/src/api/companySkills.ts +55 -0
  447. package/ui/src/api/costs.ts +60 -0
  448. package/ui/src/api/dashboard.ts +7 -0
  449. package/ui/src/api/execution-workspaces.ts +27 -0
  450. package/ui/src/api/goals.ts +12 -0
  451. package/ui/src/api/health.ts +41 -0
  452. package/ui/src/api/heartbeats.ts +62 -0
  453. package/ui/src/api/index.ts +18 -0
  454. package/ui/src/api/instanceSettings.ts +19 -0
  455. package/ui/src/api/issues.ts +115 -0
  456. package/ui/src/api/plugins.ts +424 -0
  457. package/ui/src/api/projects.ts +34 -0
  458. package/ui/src/api/routines.ts +59 -0
  459. package/ui/src/api/secrets.ts +26 -0
  460. package/ui/src/api/sidebarBadges.ts +7 -0
  461. package/ui/src/components/AccountingModelCard.tsx +69 -0
  462. package/ui/src/components/ActiveAgentsPanel.tsx +157 -0
  463. package/ui/src/components/ActivityCharts.tsx +264 -0
  464. package/ui/src/components/ActivityRow.tsx +147 -0
  465. package/ui/src/components/AgentActionButtons.tsx +51 -0
  466. package/ui/src/components/AgentConfigForm.tsx +1468 -0
  467. package/ui/src/components/AgentIconPicker.tsx +81 -0
  468. package/ui/src/components/AgentProperties.tsx +107 -0
  469. package/ui/src/components/ApprovalCard.tsx +107 -0
  470. package/ui/src/components/ApprovalPayload.tsx +134 -0
  471. package/ui/src/components/AsciiArtAnimation.tsx +355 -0
  472. package/ui/src/components/BillerSpendCard.tsx +146 -0
  473. package/ui/src/components/BreadcrumbBar.tsx +113 -0
  474. package/ui/src/components/BudgetIncidentCard.tsx +101 -0
  475. package/ui/src/components/BudgetPolicyCard.tsx +220 -0
  476. package/ui/src/components/BudgetSidebarMarker.tsx +13 -0
  477. package/ui/src/components/ClaudeSubscriptionPanel.tsx +141 -0
  478. package/ui/src/components/CodexSubscriptionPanel.tsx +158 -0
  479. package/ui/src/components/CommandPalette.tsx +239 -0
  480. package/ui/src/components/CommentThread.tsx +503 -0
  481. package/ui/src/components/CompanyPatternIcon.tsx +212 -0
  482. package/ui/src/components/CompanyRail.tsx +329 -0
  483. package/ui/src/components/CompanySwitcher.tsx +81 -0
  484. package/ui/src/components/CopyText.tsx +56 -0
  485. package/ui/src/components/DevRestartBanner.tsx +89 -0
  486. package/ui/src/components/EmptyState.tsx +27 -0
  487. package/ui/src/components/EntityRow.tsx +69 -0
  488. package/ui/src/components/FilterBar.tsx +39 -0
  489. package/ui/src/components/FinanceBillerCard.tsx +45 -0
  490. package/ui/src/components/FinanceKindCard.tsx +44 -0
  491. package/ui/src/components/FinanceTimelineCard.tsx +72 -0
  492. package/ui/src/components/GoalProperties.tsx +165 -0
  493. package/ui/src/components/GoalTree.tsx +118 -0
  494. package/ui/src/components/Identity.tsx +39 -0
  495. package/ui/src/components/InlineEditor.tsx +248 -0
  496. package/ui/src/components/InlineEntitySelector.tsx +206 -0
  497. package/ui/src/components/InstanceSidebar.tsx +53 -0
  498. package/ui/src/components/IssueDocumentsSection.tsx +892 -0
  499. package/ui/src/components/IssueProperties.tsx +621 -0
  500. package/ui/src/components/IssueRow.tsx +149 -0
  501. package/ui/src/components/IssueWorkspaceCard.tsx +404 -0
  502. package/ui/src/components/IssuesList.tsx +889 -0
  503. package/ui/src/components/JsonSchemaForm.tsx +1048 -0
  504. package/ui/src/components/KanbanBoard.tsx +275 -0
  505. package/ui/src/components/Layout.tsx +441 -0
  506. package/ui/src/components/LiveRunWidget.tsx +160 -0
  507. package/ui/src/components/MarkdownBody.test.tsx +50 -0
  508. package/ui/src/components/MarkdownBody.tsx +152 -0
  509. package/ui/src/components/MarkdownEditor.tsx +622 -0
  510. package/ui/src/components/MetricCard.tsx +53 -0
  511. package/ui/src/components/MobileBottomNav.tsx +123 -0
  512. package/ui/src/components/NewAgentDialog.tsx +223 -0
  513. package/ui/src/components/NewGoalDialog.tsx +283 -0
  514. package/ui/src/components/NewIssueDialog.tsx +1473 -0
  515. package/ui/src/components/NewProjectDialog.tsx +451 -0
  516. package/ui/src/components/OnboardingWizard.tsx +1392 -0
  517. package/ui/src/components/OpenCodeLogoIcon.tsx +22 -0
  518. package/ui/src/components/PackageFileTree.tsx +318 -0
  519. package/ui/src/components/PageSkeleton.tsx +180 -0
  520. package/ui/src/components/PageTabBar.tsx +45 -0
  521. package/ui/src/components/PathInstructionsModal.tsx +143 -0
  522. package/ui/src/components/PriorityIcon.tsx +77 -0
  523. package/ui/src/components/ProjectProperties.tsx +1127 -0
  524. package/ui/src/components/PropertiesPanel.tsx +29 -0
  525. package/ui/src/components/ProviderQuotaCard.tsx +417 -0
  526. package/ui/src/components/QuotaBar.tsx +65 -0
  527. package/ui/src/components/ReportsToPicker.tsx +127 -0
  528. package/ui/src/components/ScheduleEditor.tsx +344 -0
  529. package/ui/src/components/ScrollToBottom.tsx +79 -0
  530. package/ui/src/components/Sidebar.tsx +130 -0
  531. package/ui/src/components/SidebarAgents.tsx +146 -0
  532. package/ui/src/components/SidebarNavItem.tsx +92 -0
  533. package/ui/src/components/SidebarProjects.tsx +234 -0
  534. package/ui/src/components/SidebarSection.tsx +17 -0
  535. package/ui/src/components/StatusBadge.tsx +15 -0
  536. package/ui/src/components/StatusIcon.tsx +71 -0
  537. package/ui/src/components/SwipeToArchive.tsx +152 -0
  538. package/ui/src/components/ToastViewport.tsx +99 -0
  539. package/ui/src/components/WorktreeBanner.tsx +25 -0
  540. package/ui/src/components/agent-config-defaults.ts +31 -0
  541. package/ui/src/components/agent-config-primitives.tsx +476 -0
  542. package/ui/src/components/transcript/RunTranscriptView.test.tsx +84 -0
  543. package/ui/src/components/transcript/RunTranscriptView.tsx +1015 -0
  544. package/ui/src/components/transcript/useLiveRunTranscripts.ts +297 -0
  545. package/ui/src/components/ui/avatar.tsx +107 -0
  546. package/ui/src/components/ui/badge.tsx +48 -0
  547. package/ui/src/components/ui/breadcrumb.tsx +109 -0
  548. package/ui/src/components/ui/button.tsx +64 -0
  549. package/ui/src/components/ui/card.tsx +92 -0
  550. package/ui/src/components/ui/checkbox.tsx +32 -0
  551. package/ui/src/components/ui/collapsible.tsx +33 -0
  552. package/ui/src/components/ui/command.tsx +194 -0
  553. package/ui/src/components/ui/dialog.tsx +156 -0
  554. package/ui/src/components/ui/dropdown-menu.tsx +257 -0
  555. package/ui/src/components/ui/input.tsx +21 -0
  556. package/ui/src/components/ui/label.tsx +22 -0
  557. package/ui/src/components/ui/popover.tsx +88 -0
  558. package/ui/src/components/ui/scroll-area.tsx +56 -0
  559. package/ui/src/components/ui/select.tsx +188 -0
  560. package/ui/src/components/ui/separator.tsx +28 -0
  561. package/ui/src/components/ui/sheet.tsx +143 -0
  562. package/ui/src/components/ui/skeleton.tsx +13 -0
  563. package/ui/src/components/ui/tabs.tsx +89 -0
  564. package/ui/src/components/ui/textarea.tsx +18 -0
  565. package/ui/src/components/ui/tooltip.tsx +57 -0
  566. package/ui/src/components/visual-office/AgentAvatar.tsx +99 -0
  567. package/ui/src/components/visual-office/OfficeViewExact.tsx +417 -0
  568. package/ui/src/components/visual-office/i18n.ts +52 -0
  569. package/ui/src/components/visual-office/office-view/CliUsagePanel.tsx +240 -0
  570. package/ui/src/components/visual-office/office-view/VirtualPadOverlay.tsx +104 -0
  571. package/ui/src/components/visual-office/office-view/buildScene-break-room.ts +248 -0
  572. package/ui/src/components/visual-office/office-view/buildScene-ceo-hallway.ts +345 -0
  573. package/ui/src/components/visual-office/office-view/buildScene-department-agent.ts +242 -0
  574. package/ui/src/components/visual-office/office-view/buildScene-departments.ts +360 -0
  575. package/ui/src/components/visual-office/office-view/buildScene-final-layers.ts +113 -0
  576. package/ui/src/components/visual-office/office-view/buildScene-types.ts +91 -0
  577. package/ui/src/components/visual-office/office-view/buildScene.ts +232 -0
  578. package/ui/src/components/visual-office/office-view/drawing-core.ts +374 -0
  579. package/ui/src/components/visual-office/office-view/drawing-furniture-a.ts +338 -0
  580. package/ui/src/components/visual-office/office-view/drawing-furniture-b.ts +241 -0
  581. package/ui/src/components/visual-office/office-view/model.ts +301 -0
  582. package/ui/src/components/visual-office/office-view/officeTicker.ts +455 -0
  583. package/ui/src/components/visual-office/office-view/officeTickerRoomAndDelivery.ts +133 -0
  584. package/ui/src/components/visual-office/office-view/themes-locale.ts +460 -0
  585. package/ui/src/components/visual-office/office-view/useCliUsage.ts +37 -0
  586. package/ui/src/components/visual-office/office-view/useOfficeDeliveryEffects.ts +465 -0
  587. package/ui/src/components/visual-office/office-view/useOfficePixiRuntime.ts +282 -0
  588. package/ui/src/components/visual-office/types.ts +123 -0
  589. package/ui/src/context/BreadcrumbContext.tsx +44 -0
  590. package/ui/src/context/CompanyContext.tsx +151 -0
  591. package/ui/src/context/DialogContext.tsx +135 -0
  592. package/ui/src/context/LiveUpdatesProvider.test.ts +119 -0
  593. package/ui/src/context/LiveUpdatesProvider.tsx +760 -0
  594. package/ui/src/context/PanelContext.tsx +73 -0
  595. package/ui/src/context/SidebarContext.tsx +43 -0
  596. package/ui/src/context/ThemeContext.tsx +83 -0
  597. package/ui/src/context/ToastContext.tsx +172 -0
  598. package/ui/src/fixtures/runTranscriptFixtures.ts +226 -0
  599. package/ui/src/hooks/useAgentOrder.ts +105 -0
  600. package/ui/src/hooks/useAutosaveIndicator.ts +72 -0
  601. package/ui/src/hooks/useCompanyPageMemory.test.ts +90 -0
  602. package/ui/src/hooks/useCompanyPageMemory.ts +79 -0
  603. package/ui/src/hooks/useDateRange.ts +120 -0
  604. package/ui/src/hooks/useInboxBadge.ts +132 -0
  605. package/ui/src/hooks/useKeyboardShortcuts.ts +40 -0
  606. package/ui/src/hooks/useProjectOrder.ts +106 -0
  607. package/ui/src/index.css +770 -0
  608. package/ui/src/lib/agent-icons.ts +99 -0
  609. package/ui/src/lib/agent-order.ts +107 -0
  610. package/ui/src/lib/agent-skills-state.test.ts +90 -0
  611. package/ui/src/lib/agent-skills-state.ts +41 -0
  612. package/ui/src/lib/assignees.test.ts +92 -0
  613. package/ui/src/lib/assignees.ts +82 -0
  614. package/ui/src/lib/color-contrast.ts +107 -0
  615. package/ui/src/lib/company-export-selection.test.ts +41 -0
  616. package/ui/src/lib/company-export-selection.ts +57 -0
  617. package/ui/src/lib/company-page-memory.ts +65 -0
  618. package/ui/src/lib/company-portability-sidebar.test.ts +101 -0
  619. package/ui/src/lib/company-portability-sidebar.ts +62 -0
  620. package/ui/src/lib/company-routes.ts +88 -0
  621. package/ui/src/lib/company-selection.test.ts +34 -0
  622. package/ui/src/lib/company-selection.ts +18 -0
  623. package/ui/src/lib/groupBy.ts +11 -0
  624. package/ui/src/lib/inbox.test.ts +404 -0
  625. package/ui/src/lib/inbox.ts +292 -0
  626. package/ui/src/lib/instance-settings.test.ts +26 -0
  627. package/ui/src/lib/instance-settings.ts +25 -0
  628. package/ui/src/lib/issueDetailBreadcrumb.ts +24 -0
  629. package/ui/src/lib/legacy-agent-config.test.ts +40 -0
  630. package/ui/src/lib/legacy-agent-config.ts +17 -0
  631. package/ui/src/lib/mention-aware-link-node.test.ts +50 -0
  632. package/ui/src/lib/mention-aware-link-node.ts +67 -0
  633. package/ui/src/lib/mention-chips.ts +168 -0
  634. package/ui/src/lib/mention-deletion.test.ts +87 -0
  635. package/ui/src/lib/mention-deletion.ts +143 -0
  636. package/ui/src/lib/model-utils.ts +16 -0
  637. package/ui/src/lib/onboarding-goal.test.ts +22 -0
  638. package/ui/src/lib/onboarding-goal.ts +18 -0
  639. package/ui/src/lib/onboarding-launch.test.ts +131 -0
  640. package/ui/src/lib/onboarding-launch.ts +54 -0
  641. package/ui/src/lib/onboarding-route.test.ts +80 -0
  642. package/ui/src/lib/onboarding-route.ts +51 -0
  643. package/ui/src/lib/portable-files.ts +42 -0
  644. package/ui/src/lib/project-order.ts +71 -0
  645. package/ui/src/lib/queryKeys.ts +140 -0
  646. package/ui/src/lib/recent-assignees.ts +36 -0
  647. package/ui/src/lib/router.tsx +76 -0
  648. package/ui/src/lib/routine-trigger-patch.test.ts +72 -0
  649. package/ui/src/lib/routine-trigger-patch.ts +31 -0
  650. package/ui/src/lib/status-colors.ts +108 -0
  651. package/ui/src/lib/timeAgo.ts +31 -0
  652. package/ui/src/lib/utils.ts +168 -0
  653. package/ui/src/lib/worktree-branding.ts +65 -0
  654. package/ui/src/lib/zip.test.ts +289 -0
  655. package/ui/src/lib/zip.ts +284 -0
  656. package/ui/src/main.tsx +67 -0
  657. package/ui/src/pages/Activity.tsx +141 -0
  658. package/ui/src/pages/AgentDetail.tsx +4053 -0
  659. package/ui/src/pages/Agents.tsx +415 -0
  660. package/ui/src/pages/ApprovalDetail.tsx +369 -0
  661. package/ui/src/pages/Approvals.tsx +132 -0
  662. package/ui/src/pages/Auth.tsx +180 -0
  663. package/ui/src/pages/BoardClaim.tsx +125 -0
  664. package/ui/src/pages/CliAuth.tsx +184 -0
  665. package/ui/src/pages/Companies.tsx +297 -0
  666. package/ui/src/pages/CompanyExport.tsx +1019 -0
  667. package/ui/src/pages/CompanyImport.tsx +1355 -0
  668. package/ui/src/pages/CompanySettings.tsx +661 -0
  669. package/ui/src/pages/CompanySkills.tsx +1171 -0
  670. package/ui/src/pages/Costs.tsx +1103 -0
  671. package/ui/src/pages/Dashboard.tsx +388 -0
  672. package/ui/src/pages/DesignGuide.tsx +1330 -0
  673. package/ui/src/pages/ExecutionWorkspaceDetail.tsx +82 -0
  674. package/ui/src/pages/GoalDetail.tsx +197 -0
  675. package/ui/src/pages/Goals.tsx +63 -0
  676. package/ui/src/pages/Inbox.tsx +1291 -0
  677. package/ui/src/pages/InstanceExperimentalSettings.tsx +139 -0
  678. package/ui/src/pages/InstanceGeneralSettings.tsx +104 -0
  679. package/ui/src/pages/InstanceSettings.tsx +284 -0
  680. package/ui/src/pages/InviteLanding.tsx +320 -0
  681. package/ui/src/pages/IssueDetail.tsx +1201 -0
  682. package/ui/src/pages/Issues.tsx +116 -0
  683. package/ui/src/pages/MyIssues.tsx +72 -0
  684. package/ui/src/pages/NewAgent.tsx +353 -0
  685. package/ui/src/pages/NotFound.tsx +66 -0
  686. package/ui/src/pages/Org.tsx +132 -0
  687. package/ui/src/pages/OrgChart.tsx +447 -0
  688. package/ui/src/pages/PluginManager.tsx +510 -0
  689. package/ui/src/pages/PluginPage.tsx +156 -0
  690. package/ui/src/pages/PluginSettings.tsx +836 -0
  691. package/ui/src/pages/ProjectDetail.tsx +633 -0
  692. package/ui/src/pages/Projects.tsx +87 -0
  693. package/ui/src/pages/RoutineDetail.tsx +1022 -0
  694. package/ui/src/pages/Routines.tsx +661 -0
  695. package/ui/src/pages/RunTranscriptUxLab.tsx +334 -0
  696. package/ui/src/pages/VisualOffice.tsx +243 -0
  697. package/ui/src/plugins/bridge-init.ts +69 -0
  698. package/ui/src/plugins/bridge.ts +476 -0
  699. package/ui/src/plugins/launchers.tsx +834 -0
  700. package/ui/src/plugins/slots.tsx +855 -0
  701. package/ui/tsconfig.json +21 -0
  702. package/ui/vite.config.ts +23 -0
  703. package/ui/vitest.config.ts +14 -0
  704. package/vitest.config.ts +11 -0
@@ -0,0 +1,1468 @@
1
+ import { useState, useEffect, useRef, useMemo, useCallback } from "react";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { AGENT_ADAPTER_TYPES } from "@corporateai/shared";
4
+ import type {
5
+ Agent,
6
+ AdapterEnvironmentTestResult,
7
+ CompanySecret,
8
+ EnvBinding,
9
+ } from "@corporateai/shared";
10
+ import type { AdapterModel } from "../api/agents";
11
+ import { agentsApi } from "../api/agents";
12
+ import { secretsApi } from "../api/secrets";
13
+ import { assetsApi } from "../api/assets";
14
+ import {
15
+ DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
16
+ DEFAULT_CODEX_LOCAL_MODEL,
17
+ } from "@corporateai/adapter-codex-local";
18
+ import { DEFAULT_CURSOR_LOCAL_MODEL } from "@corporateai/adapter-cursor-local";
19
+ import { DEFAULT_GEMINI_LOCAL_MODEL } from "@corporateai/adapter-gemini-local";
20
+ import {
21
+ Popover,
22
+ PopoverContent,
23
+ PopoverTrigger,
24
+ } from "@/components/ui/popover";
25
+ import { Button } from "@/components/ui/button";
26
+ import { FolderOpen, Heart, ChevronDown, X } from "lucide-react";
27
+ import { cn } from "../lib/utils";
28
+ import { extractModelName, extractProviderId } from "../lib/model-utils";
29
+ import { queryKeys } from "../lib/queryKeys";
30
+ import { useCompany } from "../context/CompanyContext";
31
+ import {
32
+ Field,
33
+ ToggleField,
34
+ ToggleWithNumber,
35
+ CollapsibleSection,
36
+ DraftInput,
37
+ DraftNumberInput,
38
+ help,
39
+ adapterLabels,
40
+ } from "./agent-config-primitives";
41
+ import { defaultCreateValues } from "./agent-config-defaults";
42
+ import { getUIAdapter } from "../adapters";
43
+ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
44
+ import { MarkdownEditor } from "./MarkdownEditor";
45
+ import { ChoosePathButton } from "./PathInstructionsModal";
46
+ import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon";
47
+ import { ReportsToPicker } from "./ReportsToPicker";
48
+ import { shouldShowLegacyWorkingDirectoryField } from "../lib/legacy-agent-config";
49
+
50
+ /* ---- Create mode values ---- */
51
+
52
+ // Canonical type lives in @corporateai/adapter-utils; re-exported here
53
+ // so existing imports from this file keep working.
54
+ export type { CreateConfigValues } from "@corporateai/adapter-utils";
55
+ import type { CreateConfigValues } from "@corporateai/adapter-utils";
56
+
57
+ /* ---- Props ---- */
58
+
59
+ type AgentConfigFormProps = {
60
+ adapterModels?: AdapterModel[];
61
+ onDirtyChange?: (dirty: boolean) => void;
62
+ onSaveActionChange?: (save: (() => void) | null) => void;
63
+ onCancelActionChange?: (cancel: (() => void) | null) => void;
64
+ hideInlineSave?: boolean;
65
+ showAdapterTypeField?: boolean;
66
+ showAdapterTestEnvironmentButton?: boolean;
67
+ showCreateRunPolicySection?: boolean;
68
+ hideInstructionsFile?: boolean;
69
+ /** Hide the prompt template field from the Identity section (used when it's shown in a separate Prompts tab). */
70
+ hidePromptTemplate?: boolean;
71
+ /** "cards" renders each section as heading + bordered card (for settings pages). Default: "inline" (border-b dividers). */
72
+ sectionLayout?: "inline" | "cards";
73
+ } & (
74
+ | {
75
+ mode: "create";
76
+ values: CreateConfigValues;
77
+ onChange: (patch: Partial<CreateConfigValues>) => void;
78
+ }
79
+ | {
80
+ mode: "edit";
81
+ agent: Agent;
82
+ onSave: (patch: Record<string, unknown>) => void;
83
+ isSaving?: boolean;
84
+ }
85
+ );
86
+
87
+ /* ---- Edit mode overlay (dirty tracking) ---- */
88
+
89
+ interface Overlay {
90
+ identity: Record<string, unknown>;
91
+ adapterType?: string;
92
+ adapterConfig: Record<string, unknown>;
93
+ heartbeat: Record<string, unknown>;
94
+ runtime: Record<string, unknown>;
95
+ }
96
+
97
+ const emptyOverlay: Overlay = {
98
+ identity: {},
99
+ adapterConfig: {},
100
+ heartbeat: {},
101
+ runtime: {},
102
+ };
103
+
104
+ /** Stable empty object used as fallback for missing env config to avoid new-object-per-render. */
105
+ const EMPTY_ENV: Record<string, EnvBinding> = {};
106
+
107
+ function isOverlayDirty(o: Overlay): boolean {
108
+ return (
109
+ Object.keys(o.identity).length > 0 ||
110
+ o.adapterType !== undefined ||
111
+ Object.keys(o.adapterConfig).length > 0 ||
112
+ Object.keys(o.heartbeat).length > 0 ||
113
+ Object.keys(o.runtime).length > 0
114
+ );
115
+ }
116
+
117
+ /* ---- Shared input class ---- */
118
+ const inputClass =
119
+ "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
120
+
121
+ function parseCommaArgs(value: string): string[] {
122
+ return value
123
+ .split(",")
124
+ .map((item) => item.trim())
125
+ .filter(Boolean);
126
+ }
127
+
128
+ function formatArgList(value: unknown): string {
129
+ if (Array.isArray(value)) {
130
+ return value
131
+ .filter((item): item is string => typeof item === "string")
132
+ .join(", ");
133
+ }
134
+ return typeof value === "string" ? value : "";
135
+ }
136
+
137
+ const codexThinkingEffortOptions = [
138
+ { id: "", label: "Auto" },
139
+ { id: "minimal", label: "Minimal" },
140
+ { id: "low", label: "Low" },
141
+ { id: "medium", label: "Medium" },
142
+ { id: "high", label: "High" },
143
+ ] as const;
144
+
145
+ const openCodeThinkingEffortOptions = [
146
+ { id: "", label: "Auto" },
147
+ { id: "minimal", label: "Minimal" },
148
+ { id: "low", label: "Low" },
149
+ { id: "medium", label: "Medium" },
150
+ { id: "high", label: "High" },
151
+ { id: "max", label: "Max" },
152
+ ] as const;
153
+
154
+ const cursorModeOptions = [
155
+ { id: "", label: "Auto" },
156
+ { id: "plan", label: "Plan" },
157
+ { id: "ask", label: "Ask" },
158
+ ] as const;
159
+
160
+ const claudeThinkingEffortOptions = [
161
+ { id: "", label: "Auto" },
162
+ { id: "low", label: "Low" },
163
+ { id: "medium", label: "Medium" },
164
+ { id: "high", label: "High" },
165
+ ] as const;
166
+
167
+
168
+ /* ---- Form ---- */
169
+
170
+ export function AgentConfigForm(props: AgentConfigFormProps) {
171
+ const { mode, adapterModels: externalModels } = props;
172
+ const isCreate = mode === "create";
173
+ const cards = props.sectionLayout === "cards";
174
+ const showAdapterTypeField = props.showAdapterTypeField ?? true;
175
+ const showAdapterTestEnvironmentButton = props.showAdapterTestEnvironmentButton ?? true;
176
+ const showCreateRunPolicySection = props.showCreateRunPolicySection ?? true;
177
+ const hideInstructionsFile = props.hideInstructionsFile ?? false;
178
+ const { selectedCompanyId } = useCompany();
179
+ const queryClient = useQueryClient();
180
+
181
+ const { data: availableSecrets = [] } = useQuery({
182
+ queryKey: selectedCompanyId ? queryKeys.secrets.list(selectedCompanyId) : ["secrets", "none"],
183
+ queryFn: () => secretsApi.list(selectedCompanyId!),
184
+ enabled: Boolean(selectedCompanyId),
185
+ });
186
+
187
+ const createSecret = useMutation({
188
+ mutationFn: (input: { name: string; value: string }) => {
189
+ if (!selectedCompanyId) throw new Error("Select a company to create secrets");
190
+ return secretsApi.create(selectedCompanyId, input);
191
+ },
192
+ onSuccess: () => {
193
+ if (!selectedCompanyId) return;
194
+ queryClient.invalidateQueries({ queryKey: queryKeys.secrets.list(selectedCompanyId) });
195
+ },
196
+ });
197
+
198
+ const uploadMarkdownImage = useMutation({
199
+ mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
200
+ if (!selectedCompanyId) throw new Error("Select a company to upload images");
201
+ return assetsApi.uploadImage(selectedCompanyId, file, namespace);
202
+ },
203
+ });
204
+
205
+ // ---- Edit mode: overlay for dirty tracking ----
206
+ const [overlay, setOverlay] = useState<Overlay>(emptyOverlay);
207
+ const agentRef = useRef<Agent | null>(null);
208
+
209
+ // Clear overlay when agent data refreshes (after save)
210
+ useEffect(() => {
211
+ if (!isCreate) {
212
+ if (agentRef.current !== null && props.agent !== agentRef.current) {
213
+ setOverlay({ ...emptyOverlay });
214
+ }
215
+ agentRef.current = props.agent;
216
+ }
217
+ }, [isCreate, !isCreate ? props.agent : undefined]); // eslint-disable-line react-hooks/exhaustive-deps
218
+
219
+ const isDirty = !isCreate && isOverlayDirty(overlay);
220
+
221
+ /** Read effective value: overlay if dirty, else original */
222
+ function eff<T>(group: keyof Omit<Overlay, "adapterType">, field: string, original: T): T {
223
+ const o = overlay[group];
224
+ if (field in o) return o[field] as T;
225
+ return original;
226
+ }
227
+
228
+ /** Mark field dirty in overlay */
229
+ function mark(group: keyof Omit<Overlay, "adapterType">, field: string, value: unknown) {
230
+ setOverlay((prev) => ({
231
+ ...prev,
232
+ [group]: { ...prev[group], [field]: value },
233
+ }));
234
+ }
235
+
236
+ /** Build accumulated patch and send to parent */
237
+ const handleCancel = useCallback(() => {
238
+ setOverlay({ ...emptyOverlay });
239
+ }, []);
240
+
241
+ const handleSave = useCallback(() => {
242
+ if (isCreate || !isDirty) return;
243
+ const agent = props.agent;
244
+ const patch: Record<string, unknown> = {};
245
+
246
+ if (Object.keys(overlay.identity).length > 0) {
247
+ Object.assign(patch, overlay.identity);
248
+ }
249
+ if (overlay.adapterType !== undefined) {
250
+ patch.adapterType = overlay.adapterType;
251
+ // When adapter type changes, send only the new config — don't merge
252
+ // with old config since old adapter fields are meaningless for the new type
253
+ patch.adapterConfig = overlay.adapterConfig;
254
+ } else if (Object.keys(overlay.adapterConfig).length > 0) {
255
+ const existing = (agent.adapterConfig ?? {}) as Record<string, unknown>;
256
+ patch.adapterConfig = { ...existing, ...overlay.adapterConfig };
257
+ }
258
+ if (Object.keys(overlay.heartbeat).length > 0) {
259
+ const existingRc = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
260
+ const existingHb = (existingRc.heartbeat ?? {}) as Record<string, unknown>;
261
+ patch.runtimeConfig = { ...existingRc, heartbeat: { ...existingHb, ...overlay.heartbeat } };
262
+ }
263
+ if (Object.keys(overlay.runtime).length > 0) {
264
+ Object.assign(patch, overlay.runtime);
265
+ }
266
+
267
+ props.onSave(patch);
268
+ }, [isCreate, isDirty, overlay, props]);
269
+
270
+ useEffect(() => {
271
+ if (!isCreate) {
272
+ props.onDirtyChange?.(isDirty);
273
+ props.onSaveActionChange?.(handleSave);
274
+ props.onCancelActionChange?.(handleCancel);
275
+ }
276
+ }, [isCreate, isDirty, props.onDirtyChange, props.onSaveActionChange, props.onCancelActionChange, handleSave, handleCancel]);
277
+
278
+ useEffect(() => {
279
+ if (isCreate) return;
280
+ return () => {
281
+ props.onSaveActionChange?.(null);
282
+ props.onCancelActionChange?.(null);
283
+ props.onDirtyChange?.(false);
284
+ };
285
+ }, [isCreate, props.onDirtyChange, props.onSaveActionChange, props.onCancelActionChange]);
286
+
287
+ // ---- Resolve values ----
288
+ const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record<string, unknown>) : {};
289
+ const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record<string, unknown>) : {};
290
+ const heartbeat = !isCreate ? ((runtimeConfig.heartbeat ?? {}) as Record<string, unknown>) : {};
291
+
292
+ const adapterType = isCreate
293
+ ? props.values.adapterType
294
+ : overlay.adapterType ?? props.agent.adapterType;
295
+ const isLocal =
296
+ adapterType === "claude_local" ||
297
+ adapterType === "codex_local" ||
298
+ adapterType === "gemini_local" ||
299
+ adapterType === "opencode_local" ||
300
+ adapterType === "pi_local" ||
301
+ adapterType === "cursor";
302
+ const showLegacyWorkingDirectoryField =
303
+ isLocal && shouldShowLegacyWorkingDirectoryField({ isCreate, adapterConfig: config });
304
+ const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
305
+
306
+ // Fetch adapter models for the effective adapter type
307
+ const {
308
+ data: fetchedModels,
309
+ error: fetchedModelsError,
310
+ } = useQuery({
311
+ queryKey: selectedCompanyId
312
+ ? queryKeys.agents.adapterModels(selectedCompanyId, adapterType)
313
+ : ["agents", "none", "adapter-models", adapterType],
314
+ queryFn: () => agentsApi.adapterModels(selectedCompanyId!, adapterType),
315
+ enabled: Boolean(selectedCompanyId),
316
+ });
317
+ const models = fetchedModels ?? externalModels ?? [];
318
+
319
+ const { data: companyAgents = [] } = useQuery({
320
+ queryKey: selectedCompanyId ? queryKeys.agents.list(selectedCompanyId) : ["agents", "none", "list"],
321
+ queryFn: () => agentsApi.list(selectedCompanyId!),
322
+ enabled: Boolean(!isCreate && selectedCompanyId),
323
+ });
324
+
325
+ /** Props passed to adapter-specific config field components */
326
+ const adapterFieldProps = {
327
+ mode,
328
+ isCreate,
329
+ adapterType,
330
+ values: isCreate ? props.values : null,
331
+ set: isCreate ? (patch: Partial<CreateConfigValues>) => props.onChange(patch) : null,
332
+ config,
333
+ eff: eff as <T>(group: "adapterConfig", field: string, original: T) => T,
334
+ mark: mark as (group: "adapterConfig", field: string, value: unknown) => void,
335
+ models,
336
+ hideInstructionsFile,
337
+ };
338
+
339
+ // Section toggle state — advanced always starts collapsed
340
+ const [runPolicyAdvancedOpen, setRunPolicyAdvancedOpen] = useState(false);
341
+ // Popover states
342
+ const [modelOpen, setModelOpen] = useState(false);
343
+ const [thinkingEffortOpen, setThinkingEffortOpen] = useState(false);
344
+
345
+ // Create mode helpers
346
+ const val = isCreate ? props.values : null;
347
+ const set = isCreate
348
+ ? (patch: Partial<CreateConfigValues>) => props.onChange(patch)
349
+ : null;
350
+
351
+ function buildAdapterConfigForTest(): Record<string, unknown> {
352
+ if (isCreate) {
353
+ return uiAdapter.buildAdapterConfig(val!);
354
+ }
355
+ const base = config as Record<string, unknown>;
356
+ return { ...base, ...overlay.adapterConfig };
357
+ }
358
+
359
+ const testEnvironment = useMutation({
360
+ mutationFn: async () => {
361
+ if (!selectedCompanyId) {
362
+ throw new Error("Select a company to test adapter environment");
363
+ }
364
+ return agentsApi.testEnvironment(selectedCompanyId, adapterType, {
365
+ adapterConfig: buildAdapterConfigForTest(),
366
+ });
367
+ },
368
+ });
369
+
370
+ // Current model for display
371
+ const currentModelId = isCreate
372
+ ? val!.model
373
+ : eff("adapterConfig", "model", String(config.model ?? ""));
374
+
375
+ const thinkingEffortKey =
376
+ adapterType === "codex_local"
377
+ ? "modelReasoningEffort"
378
+ : adapterType === "cursor"
379
+ ? "mode"
380
+ : adapterType === "opencode_local"
381
+ ? "variant"
382
+ : "effort";
383
+ const thinkingEffortOptions =
384
+ adapterType === "codex_local"
385
+ ? codexThinkingEffortOptions
386
+ : adapterType === "cursor"
387
+ ? cursorModeOptions
388
+ : adapterType === "opencode_local"
389
+ ? openCodeThinkingEffortOptions
390
+ : claudeThinkingEffortOptions;
391
+ const currentThinkingEffort = isCreate
392
+ ? val!.thinkingEffort
393
+ : adapterType === "codex_local"
394
+ ? eff(
395
+ "adapterConfig",
396
+ "modelReasoningEffort",
397
+ String(config.modelReasoningEffort ?? config.reasoningEffort ?? ""),
398
+ )
399
+ : adapterType === "cursor"
400
+ ? eff("adapterConfig", "mode", String(config.mode ?? ""))
401
+ : adapterType === "opencode_local"
402
+ ? eff("adapterConfig", "variant", String(config.variant ?? ""))
403
+ : eff("adapterConfig", "effort", String(config.effort ?? ""));
404
+ const showThinkingEffort = adapterType !== "gemini_local";
405
+ const codexSearchEnabled = adapterType === "codex_local"
406
+ ? (isCreate ? Boolean(val!.search) : eff("adapterConfig", "search", Boolean(config.search)))
407
+ : false;
408
+ const effectiveRuntimeConfig = useMemo(() => {
409
+ if (isCreate) {
410
+ return {
411
+ heartbeat: {
412
+ enabled: val!.heartbeatEnabled,
413
+ intervalSec: val!.intervalSec,
414
+ },
415
+ };
416
+ }
417
+ const mergedHeartbeat = {
418
+ ...(runtimeConfig.heartbeat && typeof runtimeConfig.heartbeat === "object"
419
+ ? runtimeConfig.heartbeat as Record<string, unknown>
420
+ : {}),
421
+ ...overlay.heartbeat,
422
+ };
423
+ return {
424
+ ...runtimeConfig,
425
+ heartbeat: mergedHeartbeat,
426
+ };
427
+ }, [isCreate, overlay.heartbeat, runtimeConfig, val]);
428
+ return (
429
+ <div className={cn("relative", cards && "space-y-6")}>
430
+ {/* ---- Floating Save button (edit mode, when dirty) ---- */}
431
+ {isDirty && !props.hideInlineSave && (
432
+ <div className="sticky top-0 z-10 flex items-center justify-end px-4 py-2 bg-background/90 backdrop-blur-sm border-b border-primary/20">
433
+ <div className="flex items-center gap-3">
434
+ <span className="text-xs text-muted-foreground">Unsaved changes</span>
435
+ <Button
436
+ size="sm"
437
+ onClick={handleSave}
438
+ disabled={!isCreate && props.isSaving}
439
+ >
440
+ {!isCreate && props.isSaving ? "Saving..." : "Save"}
441
+ </Button>
442
+ </div>
443
+ </div>
444
+ )}
445
+
446
+ {/* ---- Identity (edit only) ---- */}
447
+ {!isCreate && (
448
+ <div className={cn(!cards && "border-b border-border")}>
449
+ {cards
450
+ ? <h3 className="text-sm font-medium mb-3">Identity</h3>
451
+ : <div className="px-4 py-2 text-xs font-medium text-muted-foreground">Identity</div>
452
+ }
453
+ <div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
454
+ <Field label="Name" hint={help.name}>
455
+ <DraftInput
456
+ value={eff("identity", "name", props.agent.name)}
457
+ onCommit={(v) => mark("identity", "name", v)}
458
+ immediate
459
+ className={inputClass}
460
+ placeholder="Agent name"
461
+ />
462
+ </Field>
463
+ <Field label="Title" hint={help.title}>
464
+ <DraftInput
465
+ value={eff("identity", "title", props.agent.title ?? "")}
466
+ onCommit={(v) => mark("identity", "title", v || null)}
467
+ immediate
468
+ className={inputClass}
469
+ placeholder="e.g. VP of Engineering"
470
+ />
471
+ </Field>
472
+ <Field label="Reports to" hint={help.reportsTo}>
473
+ <ReportsToPicker
474
+ agents={companyAgents}
475
+ value={eff("identity", "reportsTo", props.agent.reportsTo ?? null)}
476
+ onChange={(id) => mark("identity", "reportsTo", id)}
477
+ excludeAgentIds={[props.agent.id]}
478
+ chooseLabel="Choose manager…"
479
+ />
480
+ </Field>
481
+ <Field label="Capabilities" hint={help.capabilities}>
482
+ <MarkdownEditor
483
+ value={eff("identity", "capabilities", props.agent.capabilities ?? "")}
484
+ onChange={(v) => mark("identity", "capabilities", v || null)}
485
+ placeholder="Describe what this agent can do..."
486
+ contentClassName="min-h-[44px] text-sm font-mono"
487
+ imageUploadHandler={async (file) => {
488
+ const asset = await uploadMarkdownImage.mutateAsync({
489
+ file,
490
+ namespace: `agents/${props.agent.id}/capabilities`,
491
+ });
492
+ return asset.contentPath;
493
+ }}
494
+ />
495
+ </Field>
496
+ {isLocal && !props.hidePromptTemplate && (
497
+ <>
498
+ <Field label="Prompt Template" hint={help.promptTemplate}>
499
+ <MarkdownEditor
500
+ value={eff(
501
+ "adapterConfig",
502
+ "promptTemplate",
503
+ String(config.promptTemplate ?? ""),
504
+ )}
505
+ onChange={(v) => mark("adapterConfig", "promptTemplate", v ?? "")}
506
+ placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
507
+ contentClassName="min-h-[88px] text-sm font-mono"
508
+ imageUploadHandler={async (file) => {
509
+ const namespace = `agents/${props.agent.id}/prompt-template`;
510
+ const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
511
+ return asset.contentPath;
512
+ }}
513
+ />
514
+ </Field>
515
+ <div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
516
+ Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn.
517
+ </div>
518
+ </>
519
+ )}
520
+ </div>
521
+ </div>
522
+ )}
523
+
524
+ {/* ---- Adapter ---- */}
525
+ <div className={cn(!cards && (isCreate ? "border-t border-border" : "border-b border-border"))}>
526
+ <div className={cn(cards ? "flex items-center justify-between mb-3" : "px-4 py-2 flex items-center justify-between gap-2")}>
527
+ {cards
528
+ ? <h3 className="text-sm font-medium">Adapter</h3>
529
+ : <span className="text-xs font-medium text-muted-foreground">Adapter</span>
530
+ }
531
+ {showAdapterTestEnvironmentButton && (
532
+ <Button
533
+ type="button"
534
+ variant="outline"
535
+ size="sm"
536
+ className="h-7 px-2.5 text-xs"
537
+ onClick={() => testEnvironment.mutate()}
538
+ disabled={testEnvironment.isPending || !selectedCompanyId}
539
+ >
540
+ {testEnvironment.isPending ? "Testing..." : "Test environment"}
541
+ </Button>
542
+ )}
543
+ </div>
544
+ <div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
545
+ {showAdapterTypeField && (
546
+ <Field label="Adapter type" hint={help.adapterType}>
547
+ <AdapterTypeDropdown
548
+ value={adapterType}
549
+ onChange={(t) => {
550
+ if (isCreate) {
551
+ // Reset all adapter-specific fields to defaults when switching adapter type
552
+ const { adapterType: _at, ...defaults } = defaultCreateValues;
553
+ const nextValues: CreateConfigValues = { ...defaults, adapterType: t };
554
+ if (t === "codex_local") {
555
+ nextValues.model = DEFAULT_CODEX_LOCAL_MODEL;
556
+ nextValues.dangerouslyBypassSandbox =
557
+ DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
558
+ } else if (t === "gemini_local") {
559
+ nextValues.model = DEFAULT_GEMINI_LOCAL_MODEL;
560
+ } else if (t === "cursor") {
561
+ nextValues.model = DEFAULT_CURSOR_LOCAL_MODEL;
562
+ } else if (t === "opencode_local") {
563
+ nextValues.model = "";
564
+ }
565
+ set!(nextValues);
566
+ } else {
567
+ // Clear all adapter config and explicitly blank out model + effort/mode keys
568
+ // so the old adapter's values don't bleed through via eff()
569
+ setOverlay((prev) => ({
570
+ ...prev,
571
+ adapterType: t,
572
+ adapterConfig: {
573
+ model:
574
+ t === "codex_local"
575
+ ? DEFAULT_CODEX_LOCAL_MODEL
576
+ : t === "gemini_local"
577
+ ? DEFAULT_GEMINI_LOCAL_MODEL
578
+ : t === "cursor"
579
+ ? DEFAULT_CURSOR_LOCAL_MODEL
580
+ : "",
581
+ effort: "",
582
+ modelReasoningEffort: "",
583
+ variant: "",
584
+ mode: "",
585
+ ...(t === "codex_local"
586
+ ? {
587
+ dangerouslyBypassApprovalsAndSandbox:
588
+ DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
589
+ }
590
+ : {}),
591
+ },
592
+ }));
593
+ }
594
+ }}
595
+ />
596
+ </Field>
597
+ )}
598
+
599
+ {testEnvironment.error && (
600
+ <div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
601
+ {testEnvironment.error instanceof Error
602
+ ? testEnvironment.error.message
603
+ : "Environment test failed"}
604
+ </div>
605
+ )}
606
+
607
+ {testEnvironment.data && (
608
+ <AdapterEnvironmentResult result={testEnvironment.data} />
609
+ )}
610
+
611
+ {/* Working directory */}
612
+ {showLegacyWorkingDirectoryField && (
613
+ <Field label="Working directory (deprecated)" hint={help.cwd}>
614
+ <div className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5">
615
+ <FolderOpen className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
616
+ <DraftInput
617
+ value={
618
+ isCreate
619
+ ? val!.cwd
620
+ : eff("adapterConfig", "cwd", String(config.cwd ?? ""))
621
+ }
622
+ onCommit={(v) =>
623
+ isCreate
624
+ ? set!({ cwd: v })
625
+ : mark("adapterConfig", "cwd", v || undefined)
626
+ }
627
+ immediate
628
+ className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"
629
+ placeholder="/path/to/project"
630
+ />
631
+ <ChoosePathButton />
632
+ </div>
633
+ </Field>
634
+ )}
635
+
636
+ {/* Prompt template (create mode only — edit mode shows this in Identity) */}
637
+ {isLocal && isCreate && (
638
+ <>
639
+ <Field label="Prompt Template" hint={help.promptTemplate}>
640
+ <MarkdownEditor
641
+ value={val!.promptTemplate}
642
+ onChange={(v) => set!({ promptTemplate: v })}
643
+ placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
644
+ contentClassName="min-h-[88px] text-sm font-mono"
645
+ imageUploadHandler={async (file) => {
646
+ const namespace = "agents/drafts/prompt-template";
647
+ const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
648
+ return asset.contentPath;
649
+ }}
650
+ />
651
+ </Field>
652
+ <div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
653
+ Prompt template is replayed on every heartbeat. Prefer small task framing and variables like <code>{"{{ context.* }}"}</code> or <code>{"{{ run.* }}"}</code>; avoid repeating stable instructions here.
654
+ </div>
655
+ </>
656
+ )}
657
+
658
+ {/* Adapter-specific fields */}
659
+ <uiAdapter.ConfigFields {...adapterFieldProps} />
660
+ </div>
661
+
662
+ </div>
663
+
664
+ {/* ---- Permissions & Configuration ---- */}
665
+ {isLocal && (
666
+ <div className={cn(!cards && "border-b border-border")}>
667
+ {cards
668
+ ? <h3 className="text-sm font-medium mb-3">Permissions &amp; Configuration</h3>
669
+ : <div className="px-4 py-2 text-xs font-medium text-muted-foreground">Permissions &amp; Configuration</div>
670
+ }
671
+ <div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
672
+ <Field label="Command" hint={help.localCommand}>
673
+ <DraftInput
674
+ value={
675
+ isCreate
676
+ ? val!.command
677
+ : eff("adapterConfig", "command", String(config.command ?? ""))
678
+ }
679
+ onCommit={(v) =>
680
+ isCreate
681
+ ? set!({ command: v })
682
+ : mark("adapterConfig", "command", v || undefined)
683
+ }
684
+ immediate
685
+ className={inputClass}
686
+ placeholder={
687
+ adapterType === "codex_local"
688
+ ? "codex"
689
+ : adapterType === "gemini_local"
690
+ ? "gemini"
691
+ : adapterType === "pi_local"
692
+ ? "pi"
693
+ : adapterType === "cursor"
694
+ ? "agent"
695
+ : adapterType === "opencode_local"
696
+ ? "opencode"
697
+ : "claude"
698
+ }
699
+ />
700
+ </Field>
701
+
702
+ <ModelDropdown
703
+ models={models}
704
+ value={currentModelId}
705
+ onChange={(v) =>
706
+ isCreate
707
+ ? set!({ model: v })
708
+ : mark("adapterConfig", "model", v || undefined)
709
+ }
710
+ open={modelOpen}
711
+ onOpenChange={setModelOpen}
712
+ allowDefault={adapterType !== "opencode_local"}
713
+ required={adapterType === "opencode_local"}
714
+ groupByProvider={adapterType === "opencode_local"}
715
+ />
716
+ {fetchedModelsError && (
717
+ <p className="text-xs text-destructive">
718
+ {fetchedModelsError instanceof Error
719
+ ? fetchedModelsError.message
720
+ : "Failed to load adapter models."}
721
+ </p>
722
+ )}
723
+
724
+ {showThinkingEffort && (
725
+ <>
726
+ <ThinkingEffortDropdown
727
+ value={currentThinkingEffort}
728
+ options={thinkingEffortOptions}
729
+ onChange={(v) =>
730
+ isCreate
731
+ ? set!({ thinkingEffort: v })
732
+ : mark("adapterConfig", thinkingEffortKey, v || undefined)
733
+ }
734
+ open={thinkingEffortOpen}
735
+ onOpenChange={setThinkingEffortOpen}
736
+ />
737
+ {adapterType === "codex_local" &&
738
+ codexSearchEnabled &&
739
+ currentThinkingEffort === "minimal" && (
740
+ <p className="text-xs text-amber-400">
741
+ Codex may reject `minimal` thinking when search is enabled.
742
+ </p>
743
+ )}
744
+ </>
745
+ )}
746
+ {!isCreate && typeof config.bootstrapPromptTemplate === "string" && config.bootstrapPromptTemplate && (
747
+ <>
748
+ <Field label="Bootstrap prompt (legacy)" hint={help.bootstrapPrompt}>
749
+ <MarkdownEditor
750
+ value={eff(
751
+ "adapterConfig",
752
+ "bootstrapPromptTemplate",
753
+ String(config.bootstrapPromptTemplate ?? ""),
754
+ )}
755
+ onChange={(v) =>
756
+ mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
757
+ }
758
+ placeholder="Optional initial setup prompt for the first run"
759
+ contentClassName="min-h-[44px] text-sm font-mono"
760
+ imageUploadHandler={async (file) => {
761
+ const namespace = `agents/${props.agent.id}/bootstrap-prompt`;
762
+ const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
763
+ return asset.contentPath;
764
+ }}
765
+ />
766
+ </Field>
767
+ <div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
768
+ Bootstrap prompt is legacy and will be removed in a future release. Consider moving this content into the agent&apos;s prompt template or instructions file instead.
769
+ </div>
770
+ </>
771
+ )}
772
+ {adapterType === "claude_local" && (
773
+ <ClaudeLocalAdvancedFields {...adapterFieldProps} />
774
+ )}
775
+
776
+ <Field label="Extra args (comma-separated)" hint={help.extraArgs}>
777
+ <DraftInput
778
+ value={
779
+ isCreate
780
+ ? val!.extraArgs
781
+ : eff("adapterConfig", "extraArgs", formatArgList(config.extraArgs))
782
+ }
783
+ onCommit={(v) =>
784
+ isCreate
785
+ ? set!({ extraArgs: v })
786
+ : mark("adapterConfig", "extraArgs", v ? parseCommaArgs(v) : undefined)
787
+ }
788
+ immediate
789
+ className={inputClass}
790
+ placeholder="e.g. --verbose, --foo=bar"
791
+ />
792
+ </Field>
793
+
794
+ <Field label="Environment variables" hint={help.envVars}>
795
+ <EnvVarEditor
796
+ value={
797
+ isCreate
798
+ ? ((val!.envBindings ?? EMPTY_ENV) as Record<string, EnvBinding>)
799
+ : ((eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record<string, EnvBinding>))
800
+ )
801
+ }
802
+ secrets={availableSecrets}
803
+ onCreateSecret={async (name, value) => {
804
+ const created = await createSecret.mutateAsync({ name, value });
805
+ return created;
806
+ }}
807
+ onChange={(env) =>
808
+ isCreate
809
+ ? set!({ envBindings: env ?? {}, envVars: "" })
810
+ : mark("adapterConfig", "env", env)
811
+ }
812
+ />
813
+ </Field>
814
+
815
+ {/* Edit-only: timeout + grace period */}
816
+ {!isCreate && (
817
+ <>
818
+ <Field label="Timeout (sec)" hint={help.timeoutSec}>
819
+ <DraftNumberInput
820
+ value={eff(
821
+ "adapterConfig",
822
+ "timeoutSec",
823
+ Number(config.timeoutSec ?? 0),
824
+ )}
825
+ onCommit={(v) => mark("adapterConfig", "timeoutSec", v)}
826
+ immediate
827
+ className={inputClass}
828
+ />
829
+ </Field>
830
+ <Field label="Interrupt grace period (sec)" hint={help.graceSec}>
831
+ <DraftNumberInput
832
+ value={eff(
833
+ "adapterConfig",
834
+ "graceSec",
835
+ Number(config.graceSec ?? 15),
836
+ )}
837
+ onCommit={(v) => mark("adapterConfig", "graceSec", v)}
838
+ immediate
839
+ className={inputClass}
840
+ />
841
+ </Field>
842
+ </>
843
+ )}
844
+ </div>
845
+ </div>
846
+ )}
847
+
848
+ {/* ---- Run Policy ---- */}
849
+ {isCreate && showCreateRunPolicySection ? (
850
+ <div className={cn(!cards && "border-b border-border")}>
851
+ {cards
852
+ ? <h3 className="text-sm font-medium flex items-center gap-2 mb-3"><Heart className="h-3 w-3" /> Run Policy</h3>
853
+ : <div className="px-4 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2"><Heart className="h-3 w-3" /> Run Policy</div>
854
+ }
855
+ <div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
856
+ <ToggleWithNumber
857
+ label="Heartbeat on interval"
858
+ hint={help.heartbeatInterval}
859
+ checked={val!.heartbeatEnabled}
860
+ onCheckedChange={(v) => set!({ heartbeatEnabled: v })}
861
+ number={val!.intervalSec}
862
+ onNumberChange={(v) => set!({ intervalSec: v })}
863
+ numberLabel="sec"
864
+ numberPrefix="Run heartbeat every"
865
+ numberHint={help.intervalSec}
866
+ showNumber={val!.heartbeatEnabled}
867
+ />
868
+ </div>
869
+ </div>
870
+ ) : !isCreate ? (
871
+ <div className={cn(!cards && "border-b border-border")}>
872
+ {cards
873
+ ? <h3 className="text-sm font-medium flex items-center gap-2 mb-3"><Heart className="h-3 w-3" /> Run Policy</h3>
874
+ : <div className="px-4 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2"><Heart className="h-3 w-3" /> Run Policy</div>
875
+ }
876
+ <div className={cn(cards ? "border border-border rounded-lg overflow-hidden" : "")}>
877
+ <div className={cn(cards ? "p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
878
+ <ToggleWithNumber
879
+ label="Heartbeat on interval"
880
+ hint={help.heartbeatInterval}
881
+ checked={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
882
+ onCheckedChange={(v) => mark("heartbeat", "enabled", v)}
883
+ number={eff("heartbeat", "intervalSec", Number(heartbeat.intervalSec ?? 300))}
884
+ onNumberChange={(v) => mark("heartbeat", "intervalSec", v)}
885
+ numberLabel="sec"
886
+ numberPrefix="Run heartbeat every"
887
+ numberHint={help.intervalSec}
888
+ showNumber={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
889
+ />
890
+ </div>
891
+ <CollapsibleSection
892
+ title="Advanced Run Policy"
893
+ bordered={cards}
894
+ open={runPolicyAdvancedOpen}
895
+ onToggle={() => setRunPolicyAdvancedOpen(!runPolicyAdvancedOpen)}
896
+ >
897
+ <div className="space-y-3">
898
+ <ToggleField
899
+ label="Wake on demand"
900
+ hint={help.wakeOnDemand}
901
+ checked={eff(
902
+ "heartbeat",
903
+ "wakeOnDemand",
904
+ heartbeat.wakeOnDemand !== false,
905
+ )}
906
+ onChange={(v) => mark("heartbeat", "wakeOnDemand", v)}
907
+ />
908
+ <Field label="Cooldown (sec)" hint={help.cooldownSec}>
909
+ <DraftNumberInput
910
+ value={eff(
911
+ "heartbeat",
912
+ "cooldownSec",
913
+ Number(heartbeat.cooldownSec ?? 10),
914
+ )}
915
+ onCommit={(v) => mark("heartbeat", "cooldownSec", v)}
916
+ immediate
917
+ className={inputClass}
918
+ />
919
+ </Field>
920
+ <Field label="Max concurrent runs" hint={help.maxConcurrentRuns}>
921
+ <DraftNumberInput
922
+ value={eff(
923
+ "heartbeat",
924
+ "maxConcurrentRuns",
925
+ Number(heartbeat.maxConcurrentRuns ?? 1),
926
+ )}
927
+ onCommit={(v) => mark("heartbeat", "maxConcurrentRuns", v)}
928
+ immediate
929
+ className={inputClass}
930
+ />
931
+ </Field>
932
+ </div>
933
+ </CollapsibleSection>
934
+ </div>
935
+ </div>
936
+ ) : null}
937
+
938
+ </div>
939
+ );
940
+ }
941
+
942
+ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestResult }) {
943
+ const statusLabel =
944
+ result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed";
945
+ const statusClass =
946
+ result.status === "pass"
947
+ ? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
948
+ : result.status === "warn"
949
+ ? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
950
+ : "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
951
+
952
+ return (
953
+ <div className={`rounded-md border px-3 py-2 text-xs ${statusClass}`}>
954
+ <div className="flex items-center justify-between gap-2">
955
+ <span className="font-medium">{statusLabel}</span>
956
+ <span className="text-[11px] opacity-80">
957
+ {new Date(result.testedAt).toLocaleTimeString()}
958
+ </span>
959
+ </div>
960
+ <div className="mt-2 space-y-1.5">
961
+ {result.checks.map((check, idx) => (
962
+ <div key={`${check.code}-${idx}`} className="text-[11px] leading-relaxed break-words">
963
+ <span className="font-medium uppercase tracking-wide opacity-80">
964
+ {check.level}
965
+ </span>
966
+ <span className="mx-1 opacity-60">·</span>
967
+ <span>{check.message}</span>
968
+ {check.detail && <span className="block opacity-75 break-all">({check.detail})</span>}
969
+ {check.hint && <span className="block opacity-90 break-words">Hint: {check.hint}</span>}
970
+ </div>
971
+ ))}
972
+ </div>
973
+ </div>
974
+ );
975
+ }
976
+
977
+ /* ---- Internal sub-components ---- */
978
+
979
+ const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor"]);
980
+
981
+ /** Display list includes all real adapter types plus UI-only coming-soon entries. */
982
+ const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean }[] = [
983
+ ...AGENT_ADAPTER_TYPES.map((t) => ({
984
+ value: t,
985
+ label: adapterLabels[t] ?? t,
986
+ comingSoon: !ENABLED_ADAPTER_TYPES.has(t),
987
+ })),
988
+ ];
989
+
990
+ function AdapterTypeDropdown({
991
+ value,
992
+ onChange,
993
+ }: {
994
+ value: string;
995
+ onChange: (type: string) => void;
996
+ }) {
997
+ return (
998
+ <Popover>
999
+ <PopoverTrigger asChild>
1000
+ <button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
1001
+ <span className="inline-flex items-center gap-1.5">
1002
+ {value === "opencode_local" ? <OpenCodeLogoIcon className="h-3.5 w-3.5" /> : null}
1003
+ <span>{adapterLabels[value] ?? value}</span>
1004
+ </span>
1005
+ <ChevronDown className="h-3 w-3 text-muted-foreground" />
1006
+ </button>
1007
+ </PopoverTrigger>
1008
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
1009
+ {ADAPTER_DISPLAY_LIST.map((item) => (
1010
+ <button
1011
+ key={item.value}
1012
+ disabled={item.comingSoon}
1013
+ className={cn(
1014
+ "flex items-center justify-between w-full px-2 py-1.5 text-sm rounded",
1015
+ item.comingSoon
1016
+ ? "opacity-40 cursor-not-allowed"
1017
+ : "hover:bg-accent/50",
1018
+ item.value === value && !item.comingSoon && "bg-accent",
1019
+ )}
1020
+ onClick={() => {
1021
+ if (!item.comingSoon) onChange(item.value);
1022
+ }}
1023
+ >
1024
+ <span className="inline-flex items-center gap-1.5">
1025
+ {item.value === "opencode_local" ? <OpenCodeLogoIcon className="h-3.5 w-3.5" /> : null}
1026
+ <span>{item.label}</span>
1027
+ </span>
1028
+ {item.comingSoon && (
1029
+ <span className="text-[10px] text-muted-foreground">Coming soon</span>
1030
+ )}
1031
+ </button>
1032
+ ))}
1033
+ </PopoverContent>
1034
+ </Popover>
1035
+ );
1036
+ }
1037
+
1038
+ function EnvVarEditor({
1039
+ value,
1040
+ secrets,
1041
+ onCreateSecret,
1042
+ onChange,
1043
+ }: {
1044
+ value: Record<string, EnvBinding>;
1045
+ secrets: CompanySecret[];
1046
+ onCreateSecret: (name: string, value: string) => Promise<CompanySecret>;
1047
+ onChange: (env: Record<string, EnvBinding> | undefined) => void;
1048
+ }) {
1049
+ type Row = {
1050
+ key: string;
1051
+ source: "plain" | "secret";
1052
+ plainValue: string;
1053
+ secretId: string;
1054
+ };
1055
+
1056
+ function toRows(rec: Record<string, EnvBinding> | null | undefined): Row[] {
1057
+ if (!rec || typeof rec !== "object") {
1058
+ return [{ key: "", source: "plain", plainValue: "", secretId: "" }];
1059
+ }
1060
+ const entries = Object.entries(rec).map(([k, binding]) => {
1061
+ if (typeof binding === "string") {
1062
+ return {
1063
+ key: k,
1064
+ source: "plain" as const,
1065
+ plainValue: binding,
1066
+ secretId: "",
1067
+ };
1068
+ }
1069
+ if (
1070
+ typeof binding === "object" &&
1071
+ binding !== null &&
1072
+ "type" in binding &&
1073
+ (binding as { type?: unknown }).type === "secret_ref"
1074
+ ) {
1075
+ const recBinding = binding as { secretId?: unknown };
1076
+ return {
1077
+ key: k,
1078
+ source: "secret" as const,
1079
+ plainValue: "",
1080
+ secretId: typeof recBinding.secretId === "string" ? recBinding.secretId : "",
1081
+ };
1082
+ }
1083
+ if (
1084
+ typeof binding === "object" &&
1085
+ binding !== null &&
1086
+ "type" in binding &&
1087
+ (binding as { type?: unknown }).type === "plain"
1088
+ ) {
1089
+ const recBinding = binding as { value?: unknown };
1090
+ return {
1091
+ key: k,
1092
+ source: "plain" as const,
1093
+ plainValue: typeof recBinding.value === "string" ? recBinding.value : "",
1094
+ secretId: "",
1095
+ };
1096
+ }
1097
+ return {
1098
+ key: k,
1099
+ source: "plain" as const,
1100
+ plainValue: "",
1101
+ secretId: "",
1102
+ };
1103
+ });
1104
+ return [...entries, { key: "", source: "plain", plainValue: "", secretId: "" }];
1105
+ }
1106
+
1107
+ const [rows, setRows] = useState<Row[]>(() => toRows(value));
1108
+ const [sealError, setSealError] = useState<string | null>(null);
1109
+ const valueRef = useRef(value);
1110
+
1111
+ // Sync when value identity changes (overlay reset after save)
1112
+ useEffect(() => {
1113
+ if (value !== valueRef.current) {
1114
+ valueRef.current = value;
1115
+ setRows(toRows(value));
1116
+ }
1117
+ }, [value]);
1118
+
1119
+ function emit(nextRows: Row[]) {
1120
+ const rec: Record<string, EnvBinding> = {};
1121
+ for (const row of nextRows) {
1122
+ const k = row.key.trim();
1123
+ if (!k) continue;
1124
+ if (row.source === "secret") {
1125
+ if (!row.secretId) continue;
1126
+ rec[k] = { type: "secret_ref", secretId: row.secretId, version: "latest" };
1127
+ } else {
1128
+ rec[k] = { type: "plain", value: row.plainValue };
1129
+ }
1130
+ }
1131
+ onChange(Object.keys(rec).length > 0 ? rec : undefined);
1132
+ }
1133
+
1134
+ function updateRow(i: number, patch: Partial<Row>) {
1135
+ const withPatch = rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r));
1136
+ if (
1137
+ withPatch[withPatch.length - 1].key ||
1138
+ withPatch[withPatch.length - 1].plainValue ||
1139
+ withPatch[withPatch.length - 1].secretId
1140
+ ) {
1141
+ withPatch.push({ key: "", source: "plain", plainValue: "", secretId: "" });
1142
+ }
1143
+ setRows(withPatch);
1144
+ emit(withPatch);
1145
+ }
1146
+
1147
+ function removeRow(i: number) {
1148
+ const next = rows.filter((_, idx) => idx !== i);
1149
+ if (
1150
+ next.length === 0 ||
1151
+ next[next.length - 1].key ||
1152
+ next[next.length - 1].plainValue ||
1153
+ next[next.length - 1].secretId
1154
+ ) {
1155
+ next.push({ key: "", source: "plain", plainValue: "", secretId: "" });
1156
+ }
1157
+ setRows(next);
1158
+ emit(next);
1159
+ }
1160
+
1161
+ function defaultSecretName(key: string): string {
1162
+ return key
1163
+ .trim()
1164
+ .toLowerCase()
1165
+ .replace(/[^a-z0-9_]+/g, "_")
1166
+ .replace(/^_+|_+$/g, "")
1167
+ .slice(0, 64);
1168
+ }
1169
+
1170
+ async function sealRow(i: number) {
1171
+ const row = rows[i];
1172
+ if (!row) return;
1173
+ const key = row.key.trim();
1174
+ const plain = row.plainValue;
1175
+ if (!key || plain.length === 0) return;
1176
+
1177
+ const suggested = defaultSecretName(key) || "secret";
1178
+ const name = window.prompt("Secret name", suggested)?.trim();
1179
+ if (!name) return;
1180
+
1181
+ try {
1182
+ setSealError(null);
1183
+ const created = await onCreateSecret(name, plain);
1184
+ updateRow(i, {
1185
+ source: "secret",
1186
+ secretId: created.id,
1187
+ });
1188
+ } catch (err) {
1189
+ setSealError(err instanceof Error ? err.message : "Failed to create secret");
1190
+ }
1191
+ }
1192
+
1193
+ return (
1194
+ <div className="space-y-1.5">
1195
+ {rows.map((row, i) => {
1196
+ const isTrailing =
1197
+ i === rows.length - 1 &&
1198
+ !row.key &&
1199
+ !row.plainValue &&
1200
+ !row.secretId;
1201
+ return (
1202
+ <div key={i} className="flex items-center gap-1.5">
1203
+ <input
1204
+ className={cn(inputClass, "flex-[2]")}
1205
+ placeholder="KEY"
1206
+ value={row.key}
1207
+ onChange={(e) => updateRow(i, { key: e.target.value })}
1208
+ />
1209
+ <select
1210
+ className={cn(inputClass, "flex-[1] bg-background")}
1211
+ value={row.source}
1212
+ onChange={(e) =>
1213
+ updateRow(i, {
1214
+ source: e.target.value === "secret" ? "secret" : "plain",
1215
+ ...(e.target.value === "plain" ? { secretId: "" } : {}),
1216
+ })
1217
+ }
1218
+ >
1219
+ <option value="plain">Plain</option>
1220
+ <option value="secret">Secret</option>
1221
+ </select>
1222
+ {row.source === "secret" ? (
1223
+ <>
1224
+ <select
1225
+ className={cn(inputClass, "flex-[3] bg-background")}
1226
+ value={row.secretId}
1227
+ onChange={(e) => updateRow(i, { secretId: e.target.value })}
1228
+ >
1229
+ <option value="">Select secret...</option>
1230
+ {secrets.map((secret) => (
1231
+ <option key={secret.id} value={secret.id}>
1232
+ {secret.name}
1233
+ </option>
1234
+ ))}
1235
+ </select>
1236
+ <button
1237
+ type="button"
1238
+ className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0"
1239
+ onClick={() => sealRow(i)}
1240
+ disabled={!row.key.trim() || !row.plainValue}
1241
+ title="Create secret from current plain value"
1242
+ >
1243
+ New
1244
+ </button>
1245
+ </>
1246
+ ) : (
1247
+ <>
1248
+ <input
1249
+ className={cn(inputClass, "flex-[3]")}
1250
+ placeholder="value"
1251
+ value={row.plainValue}
1252
+ onChange={(e) => updateRow(i, { plainValue: e.target.value })}
1253
+ />
1254
+ <button
1255
+ type="button"
1256
+ className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0"
1257
+ onClick={() => sealRow(i)}
1258
+ disabled={!row.key.trim() || !row.plainValue}
1259
+ title="Store value as secret and replace with reference"
1260
+ >
1261
+ Seal
1262
+ </button>
1263
+ </>
1264
+ )}
1265
+ {!isTrailing ? (
1266
+ <button
1267
+ type="button"
1268
+ className="shrink-0 p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
1269
+ onClick={() => removeRow(i)}
1270
+ >
1271
+ <X className="h-3.5 w-3.5" />
1272
+ </button>
1273
+ ) : (
1274
+ <div className="w-[26px] shrink-0" />
1275
+ )}
1276
+ </div>
1277
+ );
1278
+ })}
1279
+ {sealError && <p className="text-[11px] text-destructive">{sealError}</p>}
1280
+ <p className="text-[11px] text-muted-foreground/60">
1281
+ PAPERCLIP_* variables are injected automatically at runtime.
1282
+ </p>
1283
+ </div>
1284
+ );
1285
+ }
1286
+
1287
+ function ModelDropdown({
1288
+ models,
1289
+ value,
1290
+ onChange,
1291
+ open,
1292
+ onOpenChange,
1293
+ allowDefault,
1294
+ required,
1295
+ groupByProvider,
1296
+ }: {
1297
+ models: AdapterModel[];
1298
+ value: string;
1299
+ onChange: (id: string) => void;
1300
+ open: boolean;
1301
+ onOpenChange: (open: boolean) => void;
1302
+ allowDefault: boolean;
1303
+ required: boolean;
1304
+ groupByProvider: boolean;
1305
+ }) {
1306
+ const [modelSearch, setModelSearch] = useState("");
1307
+ const selected = models.find((m) => m.id === value);
1308
+ const filteredModels = useMemo(() => {
1309
+ return models.filter((m) => {
1310
+ if (!modelSearch.trim()) return true;
1311
+ const q = modelSearch.toLowerCase();
1312
+ const provider = extractProviderId(m.id) ?? "";
1313
+ return (
1314
+ m.id.toLowerCase().includes(q) ||
1315
+ m.label.toLowerCase().includes(q) ||
1316
+ provider.toLowerCase().includes(q)
1317
+ );
1318
+ });
1319
+ }, [models, modelSearch]);
1320
+ const groupedModels = useMemo(() => {
1321
+ if (!groupByProvider) {
1322
+ return [
1323
+ {
1324
+ provider: "models",
1325
+ entries: [...filteredModels].sort((a, b) => a.id.localeCompare(b.id)),
1326
+ },
1327
+ ];
1328
+ }
1329
+ const map = new Map<string, AdapterModel[]>();
1330
+ for (const model of filteredModels) {
1331
+ const provider = extractProviderId(model.id) ?? "other";
1332
+ const group = map.get(provider) ?? [];
1333
+ group.push(model);
1334
+ map.set(provider, group);
1335
+ }
1336
+ return Array.from(map.entries())
1337
+ .sort(([a], [b]) => a.localeCompare(b))
1338
+ .map(([provider, entries]) => ({
1339
+ provider,
1340
+ entries: [...entries].sort((a, b) => a.id.localeCompare(b.id)),
1341
+ }));
1342
+ }, [filteredModels, groupByProvider]);
1343
+
1344
+ return (
1345
+ <Field label="Model" hint={help.model}>
1346
+ <Popover
1347
+ open={open}
1348
+ onOpenChange={(nextOpen) => {
1349
+ onOpenChange(nextOpen);
1350
+ if (!nextOpen) setModelSearch("");
1351
+ }}
1352
+ >
1353
+ <PopoverTrigger asChild>
1354
+ <button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
1355
+ <span className={cn(!value && "text-muted-foreground")}>
1356
+ {selected
1357
+ ? selected.label
1358
+ : value || (allowDefault ? "Default" : required ? "Select model (required)" : "Select model")}
1359
+ </span>
1360
+ <ChevronDown className="h-3 w-3 text-muted-foreground" />
1361
+ </button>
1362
+ </PopoverTrigger>
1363
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
1364
+ <input
1365
+ className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
1366
+ placeholder="Search models..."
1367
+ value={modelSearch}
1368
+ onChange={(e) => setModelSearch(e.target.value)}
1369
+ autoFocus
1370
+ />
1371
+ <div className="max-h-[240px] overflow-y-auto">
1372
+ {allowDefault && (
1373
+ <button
1374
+ className={cn(
1375
+ "flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
1376
+ !value && "bg-accent",
1377
+ )}
1378
+ onClick={() => {
1379
+ onChange("");
1380
+ onOpenChange(false);
1381
+ }}
1382
+ >
1383
+ Default
1384
+ </button>
1385
+ )}
1386
+ {groupedModels.map((group) => (
1387
+ <div key={group.provider} className="mb-1 last:mb-0">
1388
+ {groupByProvider && (
1389
+ <div className="px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground">
1390
+ {group.provider} ({group.entries.length})
1391
+ </div>
1392
+ )}
1393
+ {group.entries.map((m) => (
1394
+ <button
1395
+ key={m.id}
1396
+ className={cn(
1397
+ "flex items-center w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
1398
+ m.id === value && "bg-accent",
1399
+ )}
1400
+ onClick={() => {
1401
+ onChange(m.id);
1402
+ onOpenChange(false);
1403
+ }}
1404
+ >
1405
+ <span className="block w-full text-left truncate" title={m.id}>
1406
+ {groupByProvider ? extractModelName(m.id) : m.label}
1407
+ </span>
1408
+ </button>
1409
+ ))}
1410
+ </div>
1411
+ ))}
1412
+ {filteredModels.length === 0 && (
1413
+ <p className="px-2 py-1.5 text-xs text-muted-foreground">No models found.</p>
1414
+ )}
1415
+ </div>
1416
+ </PopoverContent>
1417
+ </Popover>
1418
+ </Field>
1419
+ );
1420
+ }
1421
+
1422
+ function ThinkingEffortDropdown({
1423
+ value,
1424
+ options,
1425
+ onChange,
1426
+ open,
1427
+ onOpenChange,
1428
+ }: {
1429
+ value: string;
1430
+ options: ReadonlyArray<{ id: string; label: string }>;
1431
+ onChange: (id: string) => void;
1432
+ open: boolean;
1433
+ onOpenChange: (open: boolean) => void;
1434
+ }) {
1435
+ const selected = options.find((option) => option.id === value) ?? options[0];
1436
+
1437
+ return (
1438
+ <Field label="Thinking effort" hint={help.thinkingEffort}>
1439
+ <Popover open={open} onOpenChange={onOpenChange}>
1440
+ <PopoverTrigger asChild>
1441
+ <button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
1442
+ <span className={cn(!value && "text-muted-foreground")}>{selected?.label ?? "Auto"}</span>
1443
+ <ChevronDown className="h-3 w-3 text-muted-foreground" />
1444
+ </button>
1445
+ </PopoverTrigger>
1446
+ <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
1447
+ {options.map((option) => (
1448
+ <button
1449
+ key={option.id || "auto"}
1450
+ className={cn(
1451
+ "flex items-center justify-between w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
1452
+ option.id === value && "bg-accent",
1453
+ )}
1454
+ onClick={() => {
1455
+ onChange(option.id);
1456
+ onOpenChange(false);
1457
+ }}
1458
+ >
1459
+ <span>{option.label}</span>
1460
+ {option.id ? <span className="text-xs text-muted-foreground font-mono">{option.id}</span> : null}
1461
+ </button>
1462
+ ))}
1463
+ </PopoverContent>
1464
+ </Popover>
1465
+ </Field>
1466
+ );
1467
+ }
1468
+