@open-mercato/ai-assistant 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (273) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +361 -0
  3. package/README.md +5 -0
  4. package/dist/index.js +154 -0
  5. package/dist/index.js.map +2 -2
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
  12. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
  13. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
  14. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
  15. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
  16. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
  17. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
  18. package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
  19. package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
  20. package/dist/modules/ai_assistant/ai-tools.js +14 -0
  21. package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
  22. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
  23. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
  24. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
  25. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
  26. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
  27. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
  28. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
  29. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
  30. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
  31. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
  32. package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
  33. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
  34. package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
  35. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
  36. package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
  37. package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
  38. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
  39. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
  40. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
  41. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
  42. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
  43. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
  44. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
  45. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
  46. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
  47. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
  48. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
  49. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
  50. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
  51. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
  52. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
  53. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
  54. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
  55. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
  56. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
  57. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
  58. package/dist/modules/ai_assistant/cli.js +78 -12
  59. package/dist/modules/ai_assistant/cli.js.map +2 -2
  60. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
  61. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
  62. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
  63. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
  64. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
  65. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
  66. package/dist/modules/ai_assistant/data/entities.js +228 -0
  67. package/dist/modules/ai_assistant/data/entities.js.map +7 -0
  68. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
  69. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
  70. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
  71. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
  72. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
  73. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
  74. package/dist/modules/ai_assistant/events.js +33 -0
  75. package/dist/modules/ai_assistant/events.js.map +7 -0
  76. package/dist/modules/ai_assistant/i18n/de.json +252 -0
  77. package/dist/modules/ai_assistant/i18n/en.json +252 -0
  78. package/dist/modules/ai_assistant/i18n/es.json +252 -0
  79. package/dist/modules/ai_assistant/i18n/pl.json +252 -0
  80. package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
  81. package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
  83. package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
  85. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
  87. package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
  89. package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
  91. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
  93. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
  95. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
  97. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
  99. package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
  101. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
  103. package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
  105. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
  107. package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
  109. package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
  111. package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
  113. package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
  115. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
  117. package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
  119. package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
  121. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
  123. package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
  125. package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
  127. package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
  128. package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
  129. package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
  130. package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
  131. package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
  132. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
  133. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
  134. package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
  135. package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
  136. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
  137. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
  138. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
  139. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
  140. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
  141. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
  142. package/dist/modules/ai_assistant/setup.js +36 -0
  143. package/dist/modules/ai_assistant/setup.js.map +2 -2
  144. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
  145. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
  146. package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
  147. package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
  148. package/generated/entities/ai_pending_action/index.ts +24 -0
  149. package/generated/entities.ids.generated.ts +13 -0
  150. package/generated/entity-fields-registry.ts +57 -0
  151. package/jest.config.cjs +7 -0
  152. package/package.json +4 -4
  153. package/src/index.ts +215 -0
  154. package/src/modules/ai_assistant/__integration__/README.md +5 -0
  155. package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
  156. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
  157. package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
  158. package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
  159. package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
  160. package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
  161. package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
  162. package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
  163. package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
  164. package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
  165. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
  166. package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
  167. package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
  168. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
  169. package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
  170. package/src/modules/ai_assistant/ai-tools.ts +21 -0
  171. package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
  172. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
  173. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
  174. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
  175. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
  176. package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
  177. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
  178. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
  179. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
  180. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
  181. package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
  182. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
  183. package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
  184. package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
  185. package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
  186. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
  187. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
  188. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
  189. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
  190. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
  191. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
  192. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
  193. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
  194. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
  195. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
  196. package/src/modules/ai_assistant/cli.ts +99 -24
  197. package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
  198. package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
  199. package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
  200. package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
  201. package/src/modules/ai_assistant/data/entities.ts +270 -0
  202. package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
  203. package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
  204. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
  205. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
  206. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
  207. package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
  208. package/src/modules/ai_assistant/events.ts +112 -0
  209. package/src/modules/ai_assistant/i18n/de.json +252 -0
  210. package/src/modules/ai_assistant/i18n/en.json +252 -0
  211. package/src/modules/ai_assistant/i18n/es.json +252 -0
  212. package/src/modules/ai_assistant/i18n/pl.json +252 -0
  213. package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
  214. package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
  215. package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
  216. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
  217. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
  218. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
  219. package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
  220. package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
  221. package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
  222. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
  223. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
  224. package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
  225. package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
  226. package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
  227. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
  228. package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
  229. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
  230. package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
  231. package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
  232. package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
  233. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
  234. package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
  235. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
  236. package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
  237. package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
  238. package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
  239. package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
  240. package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
  241. package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
  242. package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
  243. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
  244. package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
  245. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
  246. package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
  247. package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
  248. package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
  249. package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
  250. package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
  251. package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
  252. package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
  253. package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
  254. package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
  255. package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
  256. package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
  257. package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
  258. package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
  259. package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
  260. package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
  261. package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
  262. package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
  263. package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
  264. package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
  265. package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
  266. package/src/modules/ai_assistant/lib/types.ts +105 -2
  267. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
  268. package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
  269. package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
  270. package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
  271. package/src/modules/ai_assistant/setup.ts +53 -0
  272. package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
  273. package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
@@ -0,0 +1,1419 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
5
+ import {
6
+ AlertCircle,
7
+ Bot,
8
+ BookOpen,
9
+ CheckCircle2,
10
+ History,
11
+ Image as ImageIcon,
12
+ FileText,
13
+ Loader2,
14
+ Lock,
15
+ Paperclip,
16
+ RefreshCcw,
17
+ Save,
18
+ ShieldAlert,
19
+ Trash2,
20
+ Wand2,
21
+ Wrench,
22
+ } from 'lucide-react'
23
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
24
+ import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
25
+ import { Badge } from '@open-mercato/ui/primitives/badge'
26
+ import { Button } from '@open-mercato/ui/primitives/button'
27
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
28
+ import { Label } from '@open-mercato/ui/primitives/label'
29
+ import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'
30
+ import { Switch } from '@open-mercato/ui/primitives/switch'
31
+ import { Textarea } from '@open-mercato/ui/primitives/textarea'
32
+ import {
33
+ Tooltip,
34
+ TooltipContent,
35
+ TooltipProvider,
36
+ TooltipTrigger,
37
+ } from '@open-mercato/ui/primitives/tooltip'
38
+ import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
39
+ import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
40
+ import { useAiShortcuts } from '@open-mercato/ui/ai'
41
+
42
+ // Step 4.6: the <select>-based agent picker is deliberately duplicated between the
43
+ // playground and this settings page. Duplicated markup is under the 50-line
44
+ // threshold, so extraction stays deferred per the Step 4.6 brief.
45
+
46
+ type AgentTool = {
47
+ name: string
48
+ displayName: string
49
+ isMutation: boolean
50
+ registered: boolean
51
+ }
52
+
53
+ type AgentSettings = {
54
+ id: string
55
+ moduleId: string
56
+ label: string
57
+ description: string
58
+ systemPrompt: string
59
+ executionMode: 'chat' | 'object'
60
+ mutationPolicy: string
61
+ readOnly: boolean
62
+ maxSteps: number | null
63
+ allowedTools: string[]
64
+ tools: AgentTool[]
65
+ requiredFeatures: string[]
66
+ acceptedMediaTypes: string[]
67
+ hasOutputSchema: boolean
68
+ }
69
+
70
+ type AgentsResponse = {
71
+ agents: AgentSettings[]
72
+ total: number
73
+ }
74
+
75
+ const PROMPT_SECTION_IDS = [
76
+ 'role',
77
+ 'scope',
78
+ 'data',
79
+ 'tools',
80
+ 'attachments',
81
+ 'mutationPolicy',
82
+ 'responseStyle',
83
+ 'overrides',
84
+ ] as const
85
+
86
+ type PromptSectionId = (typeof PROMPT_SECTION_IDS)[number]
87
+
88
+ const mutationPolicyStatusMap: StatusMap<string> = {
89
+ 'read-only': 'neutral',
90
+ 'confirm-required': 'warning',
91
+ 'destructive-confirm-required': 'error',
92
+ }
93
+
94
+ const executionModeStatusMap: StatusMap<'chat' | 'object'> = {
95
+ chat: 'info',
96
+ object: 'success',
97
+ }
98
+
99
+ const mediaTypeIconMap: Record<string, React.ElementType> = {
100
+ image: ImageIcon,
101
+ pdf: FileText,
102
+ file: Paperclip,
103
+ }
104
+
105
+ async function fetchAgents(): Promise<AgentsResponse> {
106
+ const { result, status } = await apiCallOrThrow<AgentsResponse>(
107
+ '/api/ai_assistant/ai/agents',
108
+ { method: 'GET', credentials: 'include' },
109
+ { errorMessage: 'Failed to load agents' },
110
+ )
111
+ if (!result) throw new Error(`Failed to load agents (${status})`)
112
+ return result
113
+ }
114
+
115
+ type OverrideVersion = {
116
+ id: string
117
+ agentId: string
118
+ version: number
119
+ sections: Record<string, string>
120
+ notes: string | null
121
+ createdByUserId: string | null
122
+ createdAt: string
123
+ updatedAt: string
124
+ }
125
+
126
+ type OverrideResponse = {
127
+ agentId: string
128
+ override: OverrideVersion | null
129
+ versions: OverrideVersion[]
130
+ }
131
+
132
+ async function fetchOverride(agentId: string): Promise<OverrideResponse> {
133
+ const { result, status } = await apiCallOrThrow<OverrideResponse>(
134
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agentId)}/prompt-override`,
135
+ { method: 'GET', credentials: 'include' },
136
+ { errorMessage: 'Failed to load prompt override' },
137
+ )
138
+ if (!result) throw new Error(`Failed to load prompt override (${status})`)
139
+ return result
140
+ }
141
+
142
+ type MutationPolicy = 'read-only' | 'confirm-required' | 'destructive-confirm-required'
143
+
144
+ const MUTATION_POLICY_OPTIONS: MutationPolicy[] = [
145
+ 'read-only',
146
+ 'destructive-confirm-required',
147
+ 'confirm-required',
148
+ ]
149
+
150
+ // Higher number = less restrictive. Mirrors
151
+ // `lib/agent-policy.ts#POLICY_RESTRICTIVENESS` — UI must match the server's
152
+ // escalation guard so disabled options line up with 400 responses.
153
+ const POLICY_RESTRICTIVENESS_UI: Record<MutationPolicy, number> = {
154
+ 'read-only': 0,
155
+ 'destructive-confirm-required': 1,
156
+ 'confirm-required': 2,
157
+ }
158
+
159
+ type MutationPolicyOverrideRow = {
160
+ id: string
161
+ agentId: string
162
+ mutationPolicy: MutationPolicy
163
+ notes: string | null
164
+ createdByUserId: string | null
165
+ createdAt: string
166
+ updatedAt: string
167
+ }
168
+
169
+ type MutationPolicyResponse = {
170
+ agentId: string
171
+ codeDeclared: MutationPolicy
172
+ override: MutationPolicyOverrideRow | null
173
+ }
174
+
175
+ async function fetchMutationPolicy(agentId: string): Promise<MutationPolicyResponse> {
176
+ const { result, status } = await apiCallOrThrow<MutationPolicyResponse>(
177
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agentId)}/mutation-policy`,
178
+ { method: 'GET', credentials: 'include' },
179
+ { errorMessage: 'Failed to load mutation policy' },
180
+ )
181
+ if (!result) throw new Error(`Failed to load mutation policy (${status})`)
182
+ return result
183
+ }
184
+
185
+ function SettingsLoading({ message }: { message: string }) {
186
+ return (
187
+ <div
188
+ className="flex items-center gap-2 rounded-lg border border-border bg-background p-4 text-sm text-muted-foreground"
189
+ role="status"
190
+ >
191
+ <Loader2 className="size-4 animate-spin" aria-hidden />
192
+ <span>{message}</span>
193
+ </div>
194
+ )
195
+ }
196
+
197
+ function EmptyAgents() {
198
+ const t = useT()
199
+ return (
200
+ <EmptyState
201
+ icon={<Bot className="size-6" aria-hidden />}
202
+ title={t(
203
+ 'ai_assistant.agents.empty.title',
204
+ 'No AI agents are registered for your role yet.',
205
+ )}
206
+ description={t(
207
+ 'ai_assistant.agents.empty.description',
208
+ 'Declare agents inside `packages/<module>/src/modules/<module>/ai-agents.ts`, run `yarn generate`, and ensure the caller holds the agent\'s required features.',
209
+ )}
210
+ >
211
+ <div className="mt-2 inline-flex items-center gap-2 text-xs text-muted-foreground">
212
+ <BookOpen className="size-3" aria-hidden />
213
+ <span>
214
+ {t(
215
+ 'ai_assistant.agents.empty.docLabel',
216
+ 'See packages/ai-assistant/AGENTS.md for the agent definition reference.',
217
+ )}
218
+ </span>
219
+ </div>
220
+ </EmptyState>
221
+ )
222
+ }
223
+
224
+ function PromptSectionEditor({
225
+ sectionId,
226
+ defaultText,
227
+ overrideText,
228
+ override,
229
+ onToggleOverride,
230
+ onOverrideChange,
231
+ onSaveShortcut,
232
+ }: {
233
+ sectionId: PromptSectionId
234
+ defaultText: string
235
+ overrideText: string
236
+ override: boolean
237
+ onToggleOverride: (next: boolean) => void
238
+ onOverrideChange: (next: string) => void
239
+ onSaveShortcut: () => void
240
+ }) {
241
+ const t = useT()
242
+ const sectionLabel = t(
243
+ `ai_assistant.agents.prompt.sections.${sectionId}`,
244
+ sectionId.charAt(0).toUpperCase() + sectionId.slice(1),
245
+ )
246
+ const textareaId = `ai-agent-prompt-${sectionId}`
247
+
248
+ const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)
249
+ const { handleKeyDown } = useAiShortcuts({
250
+ onSubmit: onSaveShortcut,
251
+ onCancel: () => {
252
+ textareaRef.current?.blur()
253
+ },
254
+ })
255
+
256
+ return (
257
+ <div
258
+ className="flex flex-col gap-2 rounded-md border border-border bg-muted/20 p-3"
259
+ data-ai-agent-prompt-section={sectionId}
260
+ >
261
+ <div className="flex items-center justify-between gap-3">
262
+ <div className="flex flex-col">
263
+ <span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
264
+ {sectionLabel}
265
+ </span>
266
+ <span className="text-xs text-muted-foreground">
267
+ {override
268
+ ? t(
269
+ 'ai_assistant.agents.prompt.overrideModeLabel',
270
+ 'Override mode — replaces the default when persistence lands.',
271
+ )
272
+ : t(
273
+ 'ai_assistant.agents.prompt.defaultModeLabel',
274
+ 'Default — shipped with the agent definition.',
275
+ )}
276
+ </span>
277
+ </div>
278
+ <div className="flex items-center gap-2">
279
+ <Label htmlFor={`${textareaId}-toggle`} className="text-xs">
280
+ {t('ai_assistant.agents.prompt.toggleOverride', 'Override')}
281
+ </Label>
282
+ <Switch
283
+ id={`${textareaId}-toggle`}
284
+ checked={override}
285
+ onCheckedChange={(next: boolean) => onToggleOverride(next)}
286
+ aria-label={t('ai_assistant.agents.prompt.toggleOverride', 'Override')}
287
+ data-ai-agent-prompt-toggle={sectionId}
288
+ />
289
+ </div>
290
+ </div>
291
+ {override ? (
292
+ <Textarea
293
+ id={textareaId}
294
+ ref={textareaRef}
295
+ rows={4}
296
+ value={overrideText}
297
+ onChange={(event) => onOverrideChange(event.target.value)}
298
+ onKeyDown={handleKeyDown}
299
+ className="resize-y font-mono text-xs"
300
+ placeholder={t(
301
+ 'ai_assistant.agents.prompt.overridePlaceholder',
302
+ 'Write the replacement text for this section...',
303
+ )}
304
+ aria-label={`${sectionLabel} override`}
305
+ data-ai-agent-prompt-override={sectionId}
306
+ />
307
+ ) : (
308
+ <pre
309
+ className="max-h-40 overflow-auto rounded border border-border bg-background p-2 text-xs font-mono whitespace-pre-wrap"
310
+ data-ai-agent-prompt-default={sectionId}
311
+ >
312
+ {defaultText}
313
+ </pre>
314
+ )}
315
+ </div>
316
+ )
317
+ }
318
+
319
+ function ToolRow({ tool }: { tool: AgentTool }) {
320
+ const t = useT()
321
+ return (
322
+ <div
323
+ className="flex items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
324
+ data-ai-agent-tool-row={tool.name}
325
+ >
326
+ <div className="flex items-start gap-2 min-w-0">
327
+ <Wrench className="mt-0.5 size-4 text-muted-foreground" aria-hidden />
328
+ <div className="flex flex-col min-w-0">
329
+ <span className="truncate text-sm font-medium">{tool.displayName}</span>
330
+ <span className="truncate text-xs font-mono text-muted-foreground">{tool.name}</span>
331
+ </div>
332
+ </div>
333
+ <div className="flex items-center gap-2 flex-shrink-0">
334
+ {tool.isMutation ? (
335
+ <StatusBadge variant="warning" dot>
336
+ {t('ai_assistant.agents.tools.mutationBadge', 'Mutation')}
337
+ </StatusBadge>
338
+ ) : (
339
+ <StatusBadge variant="neutral" dot>
340
+ {t('ai_assistant.agents.tools.readBadge', 'Read')}
341
+ </StatusBadge>
342
+ )}
343
+ {!tool.registered ? (
344
+ <StatusBadge variant="error" dot>
345
+ {t('ai_assistant.agents.tools.missingBadge', 'Missing')}
346
+ </StatusBadge>
347
+ ) : null}
348
+ <Tooltip>
349
+ <TooltipTrigger asChild>
350
+ <span className="inline-flex items-center gap-1">
351
+ <Label
352
+ htmlFor={`ai-agent-tool-${tool.name}`}
353
+ className="text-xs text-muted-foreground"
354
+ >
355
+ {t('ai_assistant.agents.tools.enabledLabel', 'Enabled')}
356
+ </Label>
357
+ <Switch
358
+ id={`ai-agent-tool-${tool.name}`}
359
+ checked
360
+ disabled
361
+ aria-label={t('ai_assistant.agents.tools.enabledLabel', 'Enabled')}
362
+ data-ai-agent-tool-switch={tool.name}
363
+ />
364
+ </span>
365
+ </TooltipTrigger>
366
+ <TooltipContent>
367
+ {t(
368
+ 'ai_assistant.agents.tools.tooltipDisabled',
369
+ 'Editable after Phase 3 lands mutation policy controls.',
370
+ )}
371
+ </TooltipContent>
372
+ </Tooltip>
373
+ </div>
374
+ </div>
375
+ )
376
+ }
377
+
378
+ function AttachmentPolicyBadges({ mediaTypes }: { mediaTypes: string[] }) {
379
+ const t = useT()
380
+ if (!mediaTypes.length) {
381
+ return (
382
+ <Badge variant="neutral">
383
+ {t('ai_assistant.agents.attachments.noneBadge', 'No attachments accepted')}
384
+ </Badge>
385
+ )
386
+ }
387
+ return (
388
+ <div className="flex flex-wrap items-center gap-2">
389
+ {mediaTypes.map((mediaType) => {
390
+ const Icon = mediaTypeIconMap[mediaType] ?? Paperclip
391
+ return (
392
+ <Badge
393
+ key={mediaType}
394
+ variant="info"
395
+ className="gap-1.5"
396
+ data-ai-agent-attachment-badge={mediaType}
397
+ >
398
+ <Icon className="size-3" aria-hidden />
399
+ <span className="font-mono text-xs">{mediaType}</span>
400
+ </Badge>
401
+ )
402
+ })}
403
+ </div>
404
+ )
405
+ }
406
+
407
+ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
408
+ const t = useT()
409
+ const queryClient = useQueryClient()
410
+
411
+ const query = useQuery<MutationPolicyResponse>({
412
+ queryKey: ['ai_assistant', 'agent_settings', 'mutation_policy', agent.id],
413
+ queryFn: () => fetchMutationPolicy(agent.id),
414
+ retry: false,
415
+ })
416
+
417
+ const codeDeclared = (query.data?.codeDeclared ?? (agent.mutationPolicy as MutationPolicy))
418
+ const currentOverride = query.data?.override ?? null
419
+
420
+ const [selected, setSelected] = React.useState<MutationPolicy | null>(null)
421
+ const [isSaving, setIsSaving] = React.useState(false)
422
+ const [isClearing, setIsClearing] = React.useState(false)
423
+ const [state, setState] = React.useState<
424
+ | { kind: 'idle' }
425
+ | { kind: 'success'; message: string }
426
+ | { kind: 'error'; message: string }
427
+ >({ kind: 'idle' })
428
+
429
+ React.useEffect(() => {
430
+ setSelected(currentOverride?.mutationPolicy ?? null)
431
+ setState({ kind: 'idle' })
432
+ }, [agent.id, currentOverride?.mutationPolicy])
433
+
434
+ const codeRank = POLICY_RESTRICTIVENESS_UI[codeDeclared] ?? 0
435
+ const effectivePolicy: MutationPolicy = (() => {
436
+ if (!currentOverride) return codeDeclared
437
+ const overrideRank = POLICY_RESTRICTIVENESS_UI[currentOverride.mutationPolicy]
438
+ return overrideRank < codeRank ? currentOverride.mutationPolicy : codeDeclared
439
+ })()
440
+
441
+ const hasChange = selected !== null && selected !== (currentOverride?.mutationPolicy ?? null)
442
+
443
+ const save = React.useCallback(async () => {
444
+ if (!selected || isSaving) return
445
+ setIsSaving(true)
446
+ setState({ kind: 'idle' })
447
+ try {
448
+ const { ok, status, result } = await apiCall<{
449
+ ok?: boolean
450
+ error?: string
451
+ code?: string
452
+ codeDeclared?: MutationPolicy
453
+ requested?: MutationPolicy
454
+ }>(
455
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/mutation-policy`,
456
+ {
457
+ method: 'POST',
458
+ headers: { 'content-type': 'application/json' },
459
+ credentials: 'include',
460
+ body: JSON.stringify({ mutationPolicy: selected }),
461
+ },
462
+ )
463
+ const payload = result ?? {}
464
+ if (!ok) {
465
+ const message =
466
+ payload.code === 'escalation_not_allowed'
467
+ ? (payload.error ??
468
+ t(
469
+ 'ai_assistant.agents.mutation_policy.errors.escalationNotAllowed',
470
+ 'Cannot upgrade beyond the agent\'s declared policy — this is a code-level change.',
471
+ ))
472
+ : (payload.error ??
473
+ `Failed to save mutation policy (${status}).`)
474
+ setState({ kind: 'error', message })
475
+ return
476
+ }
477
+ setState({
478
+ kind: 'success',
479
+ message: t(
480
+ 'ai_assistant.agents.mutation_policy.savedMessage',
481
+ 'Mutation policy override saved.',
482
+ ),
483
+ })
484
+ await queryClient.invalidateQueries({
485
+ queryKey: ['ai_assistant', 'agent_settings', 'mutation_policy', agent.id],
486
+ })
487
+ } catch (err) {
488
+ setState({
489
+ kind: 'error',
490
+ message: err instanceof Error ? err.message : String(err),
491
+ })
492
+ } finally {
493
+ setIsSaving(false)
494
+ }
495
+ }, [agent.id, isSaving, queryClient, selected, t])
496
+
497
+ const clear = React.useCallback(async () => {
498
+ if (isClearing) return
499
+ setIsClearing(true)
500
+ setState({ kind: 'idle' })
501
+ try {
502
+ const { ok, status, result } = await apiCall<{
503
+ ok?: boolean
504
+ error?: string
505
+ }>(
506
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/mutation-policy`,
507
+ {
508
+ method: 'DELETE',
509
+ credentials: 'include',
510
+ },
511
+ )
512
+ const payload = result ?? {}
513
+ if (!ok) {
514
+ setState({
515
+ kind: 'error',
516
+ message: payload.error ?? `Failed to clear override (${status}).`,
517
+ })
518
+ return
519
+ }
520
+ setState({
521
+ kind: 'success',
522
+ message: t(
523
+ 'ai_assistant.agents.mutation_policy.clearedMessage',
524
+ 'Mutation policy override cleared; agent is using its code-declared policy.',
525
+ ),
526
+ })
527
+ await queryClient.invalidateQueries({
528
+ queryKey: ['ai_assistant', 'agent_settings', 'mutation_policy', agent.id],
529
+ })
530
+ } catch (err) {
531
+ setState({
532
+ kind: 'error',
533
+ message: err instanceof Error ? err.message : String(err),
534
+ })
535
+ } finally {
536
+ setIsClearing(false)
537
+ }
538
+ }, [agent.id, isClearing, queryClient, t])
539
+
540
+ return (
541
+ <section
542
+ className="rounded-lg border border-border bg-background p-4"
543
+ data-ai-agent-mutation-policy={agent.id}
544
+ >
545
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-3">
546
+ <div className="flex items-center gap-2">
547
+ <ShieldAlert className="size-4 text-muted-foreground" aria-hidden />
548
+ <div>
549
+ <h3 className="text-sm font-semibold">
550
+ {t('ai_assistant.agents.mutation_policy.title', 'Mutation policy')}
551
+ </h3>
552
+ <p className="text-xs text-muted-foreground">
553
+ {t(
554
+ 'ai_assistant.agents.mutation_policy.subtitle',
555
+ 'Downgrade this agent\'s mutation capability per tenant. Upgrading beyond the code-declared policy is blocked by the server.',
556
+ )}
557
+ </p>
558
+ </div>
559
+ </div>
560
+ <StatusBadge
561
+ variant={mutationPolicyStatusMap[effectivePolicy] ?? 'neutral'}
562
+ dot
563
+ data-ai-agent-mutation-policy-effective
564
+ >
565
+ {effectivePolicy}
566
+ </StatusBadge>
567
+ </header>
568
+
569
+ <div className="mt-3 flex flex-col gap-3">
570
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
571
+ <div>
572
+ <span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
573
+ {t('ai_assistant.agents.mutation_policy.codeDeclared', 'Code-declared')}
574
+ </span>
575
+ <div className="mt-1 flex items-center gap-2">
576
+ <StatusBadge
577
+ variant={mutationPolicyStatusMap[codeDeclared] ?? 'neutral'}
578
+ dot
579
+ data-ai-agent-mutation-policy-code-declared
580
+ >
581
+ {codeDeclared}
582
+ </StatusBadge>
583
+ <Lock className="size-3 text-muted-foreground" aria-hidden />
584
+ <span className="text-xs text-muted-foreground">
585
+ {t(
586
+ 'ai_assistant.agents.mutation_policy.codeDeclaredHint',
587
+ 'Compiled into the agent definition.',
588
+ )}
589
+ </span>
590
+ </div>
591
+ </div>
592
+ <div>
593
+ <span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
594
+ {t('ai_assistant.agents.mutation_policy.tenantOverride', 'Tenant override')}
595
+ </span>
596
+ <div className="mt-1">
597
+ {currentOverride ? (
598
+ <StatusBadge
599
+ variant={mutationPolicyStatusMap[currentOverride.mutationPolicy] ?? 'neutral'}
600
+ dot
601
+ data-ai-agent-mutation-policy-override-current
602
+ >
603
+ {currentOverride.mutationPolicy}
604
+ </StatusBadge>
605
+ ) : (
606
+ <span
607
+ className="text-xs text-muted-foreground"
608
+ data-ai-agent-mutation-policy-override-empty
609
+ >
610
+ {t(
611
+ 'ai_assistant.agents.mutation_policy.noOverride',
612
+ 'No override — using code-declared policy.',
613
+ )}
614
+ </span>
615
+ )}
616
+ </div>
617
+ </div>
618
+ </div>
619
+
620
+ <Alert variant="info" data-ai-agent-mutation-policy-notice>
621
+ <ShieldAlert className="size-4" aria-hidden />
622
+ <AlertTitle>
623
+ {t(
624
+ 'ai_assistant.agents.mutation_policy.noticeTitle',
625
+ 'Downgrade only — escalation is a code change',
626
+ )}
627
+ </AlertTitle>
628
+ <AlertDescription>
629
+ {t(
630
+ 'ai_assistant.agents.mutation_policy.noticeBody',
631
+ 'Overrides can only make the policy more restrictive. Options more permissive than the code-declared policy are disabled and rejected server-side.',
632
+ )}
633
+ </AlertDescription>
634
+ </Alert>
635
+
636
+ {query.isLoading ? (
637
+ <SettingsLoading
638
+ message={t(
639
+ 'ai_assistant.agents.mutation_policy.loading',
640
+ 'Loading mutation policy...',
641
+ )}
642
+ />
643
+ ) : query.isError ? (
644
+ <Alert variant="destructive" data-ai-agent-mutation-policy-load-error>
645
+ <AlertCircle className="size-4" aria-hidden />
646
+ <AlertTitle>
647
+ {t(
648
+ 'ai_assistant.agents.mutation_policy.loadErrorTitle',
649
+ 'Failed to load mutation policy',
650
+ )}
651
+ </AlertTitle>
652
+ <AlertDescription>
653
+ {query.error instanceof Error ? query.error.message : String(query.error)}
654
+ </AlertDescription>
655
+ </Alert>
656
+ ) : (
657
+ <div
658
+ className="flex flex-col gap-2"
659
+ role="radiogroup"
660
+ aria-label={t(
661
+ 'ai_assistant.agents.mutation_policy.pickerLabel',
662
+ 'Mutation policy override',
663
+ )}
664
+ data-ai-agent-mutation-policy-picker
665
+ >
666
+ {MUTATION_POLICY_OPTIONS.map((option) => {
667
+ const optionRank = POLICY_RESTRICTIVENESS_UI[option]
668
+ const wouldEscalate = optionRank > codeRank
669
+ const isSelected = selected === option
670
+ return (
671
+ <Tooltip key={option}>
672
+ <TooltipTrigger asChild>
673
+ <label
674
+ className={`flex items-start gap-3 rounded-md border px-3 py-2 text-sm cursor-pointer transition-colors ${
675
+ wouldEscalate
676
+ ? 'border-border bg-muted/30 cursor-not-allowed opacity-60'
677
+ : isSelected
678
+ ? 'border-primary bg-primary/5'
679
+ : 'border-border bg-background hover:bg-muted/40'
680
+ }`}
681
+ data-ai-agent-mutation-policy-option={option}
682
+ data-ai-agent-mutation-policy-option-disabled={wouldEscalate ? 'true' : 'false'}
683
+ >
684
+ <input
685
+ type="radio"
686
+ name={`mutation-policy-${agent.id}`}
687
+ value={option}
688
+ checked={isSelected}
689
+ disabled={wouldEscalate}
690
+ onChange={() => {
691
+ if (wouldEscalate) return
692
+ setSelected(option)
693
+ }}
694
+ className="mt-0.5"
695
+ aria-disabled={wouldEscalate}
696
+ />
697
+ <div className="flex flex-col gap-0.5 min-w-0">
698
+ <span className="flex items-center gap-2">
699
+ <StatusBadge
700
+ variant={mutationPolicyStatusMap[option] ?? 'neutral'}
701
+ dot
702
+ >
703
+ {option}
704
+ </StatusBadge>
705
+ {wouldEscalate ? (
706
+ <Lock className="size-3 text-muted-foreground" aria-hidden />
707
+ ) : null}
708
+ </span>
709
+ <span className="text-xs text-muted-foreground">
710
+ {t(
711
+ `ai_assistant.agents.mutation_policy.options.${option}`,
712
+ option,
713
+ )}
714
+ </span>
715
+ </div>
716
+ </label>
717
+ </TooltipTrigger>
718
+ {wouldEscalate ? (
719
+ <TooltipContent>
720
+ {t(
721
+ 'ai_assistant.agents.mutation_policy.escalationTooltip',
722
+ "Cannot be set above the agent's declared policy — this is a code-level change.",
723
+ )}
724
+ </TooltipContent>
725
+ ) : null}
726
+ </Tooltip>
727
+ )
728
+ })}
729
+ </div>
730
+ )}
731
+
732
+ {state.kind === 'success' ? (
733
+ <Alert variant="success" data-ai-agent-mutation-policy-state="success">
734
+ <CheckCircle2 className="size-4" aria-hidden />
735
+ <AlertTitle>
736
+ {t('ai_assistant.agents.mutation_policy.savedTitle', 'Mutation policy updated')}
737
+ </AlertTitle>
738
+ <AlertDescription>{state.message}</AlertDescription>
739
+ </Alert>
740
+ ) : null}
741
+ {state.kind === 'error' ? (
742
+ <Alert variant="destructive" data-ai-agent-mutation-policy-state="error">
743
+ <AlertCircle className="size-4" aria-hidden />
744
+ <AlertTitle>
745
+ {t(
746
+ 'ai_assistant.agents.mutation_policy.errorTitle',
747
+ 'Failed to update mutation policy',
748
+ )}
749
+ </AlertTitle>
750
+ <AlertDescription>{state.message}</AlertDescription>
751
+ </Alert>
752
+ ) : null}
753
+
754
+ <div className="flex items-center justify-end gap-2">
755
+ <Button
756
+ type="button"
757
+ size="sm"
758
+ variant="outline"
759
+ onClick={() => void clear()}
760
+ disabled={isClearing || isSaving || !currentOverride}
761
+ data-ai-agent-mutation-policy-clear
762
+ >
763
+ {isClearing ? (
764
+ <Loader2 className="size-4 animate-spin" aria-hidden />
765
+ ) : (
766
+ <Trash2 className="size-4" aria-hidden />
767
+ )}
768
+ <span>{t('ai_assistant.agents.mutation_policy.clear', 'Clear override')}</span>
769
+ </Button>
770
+ <Button
771
+ type="button"
772
+ size="sm"
773
+ onClick={() => void save()}
774
+ disabled={isSaving || isClearing || !hasChange || selected === null}
775
+ data-ai-agent-mutation-policy-save
776
+ >
777
+ {isSaving ? (
778
+ <Loader2 className="size-4 animate-spin" aria-hidden />
779
+ ) : (
780
+ <Save className="size-4" aria-hidden />
781
+ )}
782
+ <span>{t('ai_assistant.agents.mutation_policy.save', 'Save override')}</span>
783
+ </Button>
784
+ </div>
785
+ </div>
786
+ </section>
787
+ )
788
+ }
789
+
790
+ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
791
+ const t = useT()
792
+ const queryClient = useQueryClient()
793
+ const [overrideFlags, setOverrideFlags] = React.useState<Record<PromptSectionId, boolean>>(() => ({
794
+ role: false,
795
+ scope: false,
796
+ data: false,
797
+ tools: false,
798
+ attachments: false,
799
+ mutationPolicy: false,
800
+ responseStyle: false,
801
+ overrides: false,
802
+ }))
803
+ const [overrideDrafts, setOverrideDrafts] = React.useState<Record<PromptSectionId, string>>(() => ({
804
+ role: '',
805
+ scope: '',
806
+ data: '',
807
+ tools: '',
808
+ attachments: '',
809
+ mutationPolicy: '',
810
+ responseStyle: '',
811
+ overrides: '',
812
+ }))
813
+ const [isSaving, setIsSaving] = React.useState(false)
814
+ const [saveState, setSaveState] = React.useState<
815
+ | { kind: 'idle' }
816
+ | { kind: 'success'; message: string; version: number }
817
+ | { kind: 'error'; message: string }
818
+ >({ kind: 'idle' })
819
+
820
+ const overrideQuery = useQuery<OverrideResponse>({
821
+ queryKey: ['ai_assistant', 'agent_settings', 'override', agent.id],
822
+ queryFn: () => fetchOverride(agent.id),
823
+ retry: false,
824
+ })
825
+
826
+ // Reset override state whenever the selected agent changes.
827
+ React.useEffect(() => {
828
+ setOverrideFlags({
829
+ role: false,
830
+ scope: false,
831
+ data: false,
832
+ tools: false,
833
+ attachments: false,
834
+ mutationPolicy: false,
835
+ responseStyle: false,
836
+ overrides: false,
837
+ })
838
+ setOverrideDrafts({
839
+ role: '',
840
+ scope: '',
841
+ data: '',
842
+ tools: '',
843
+ attachments: '',
844
+ mutationPolicy: '',
845
+ responseStyle: '',
846
+ overrides: '',
847
+ })
848
+ setSaveState({ kind: 'idle' })
849
+ }, [agent.id])
850
+
851
+ // Hydrate drafts when the latest override lands.
852
+ React.useEffect(() => {
853
+ const latest = overrideQuery.data?.override
854
+ if (!latest) return
855
+ const nextFlags: Record<PromptSectionId, boolean> = {
856
+ role: false,
857
+ scope: false,
858
+ data: false,
859
+ tools: false,
860
+ attachments: false,
861
+ mutationPolicy: false,
862
+ responseStyle: false,
863
+ overrides: false,
864
+ }
865
+ const nextDrafts: Record<PromptSectionId, string> = {
866
+ role: '',
867
+ scope: '',
868
+ data: '',
869
+ tools: '',
870
+ attachments: '',
871
+ mutationPolicy: '',
872
+ responseStyle: '',
873
+ overrides: '',
874
+ }
875
+ for (const [rawKey, value] of Object.entries(latest.sections ?? {})) {
876
+ if (typeof value !== 'string') continue
877
+ const key = rawKey as PromptSectionId
878
+ if (PROMPT_SECTION_IDS.includes(key)) {
879
+ nextFlags[key] = true
880
+ nextDrafts[key] = value
881
+ }
882
+ }
883
+ setOverrideFlags(nextFlags)
884
+ setOverrideDrafts(nextDrafts)
885
+ }, [overrideQuery.data?.override])
886
+
887
+ const activeOverrides = React.useMemo(() => {
888
+ const payload: Record<string, string> = {}
889
+ for (const section of PROMPT_SECTION_IDS) {
890
+ if (overrideFlags[section]) {
891
+ payload[section] = overrideDrafts[section]
892
+ }
893
+ }
894
+ return payload
895
+ }, [overrideDrafts, overrideFlags])
896
+
897
+ const hasAnyOverride = Object.keys(activeOverrides).length > 0
898
+
899
+ const handleSave = React.useCallback(async () => {
900
+ if (isSaving) return
901
+ setIsSaving(true)
902
+ setSaveState({ kind: 'idle' })
903
+ try {
904
+ const { ok, status, result } = await apiCall<{
905
+ ok?: boolean
906
+ pending?: boolean
907
+ version?: number
908
+ updatedAt?: string
909
+ message?: string
910
+ error?: string
911
+ code?: string
912
+ reservedKeys?: string[]
913
+ }>(
914
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/prompt-override`,
915
+ {
916
+ method: 'POST',
917
+ headers: { 'content-type': 'application/json' },
918
+ credentials: 'include',
919
+ body: JSON.stringify({
920
+ // Send both keys so a pre-Step-5.3 server still accepts the payload.
921
+ sections: activeOverrides,
922
+ overrides: activeOverrides,
923
+ }),
924
+ },
925
+ )
926
+ const payload = result ?? {}
927
+ if (!ok) {
928
+ const message =
929
+ payload.code === 'reserved_key'
930
+ ? t(
931
+ 'ai_assistant.agents.override.errors.reservedKey',
932
+ 'Prompt overrides cannot modify policy fields (mutationPolicy, readOnly, allowedTools, acceptedMediaTypes). Remove those sections and retry.',
933
+ )
934
+ : (payload.error ?? `Failed to save overrides (${status}).`)
935
+ setSaveState({ kind: 'error', message })
936
+ return
937
+ }
938
+ if (payload.ok === true && typeof payload.version === 'number') {
939
+ setSaveState({
940
+ kind: 'success',
941
+ version: payload.version,
942
+ message: t(
943
+ 'ai_assistant.agents.override.savedMessage',
944
+ 'Prompt override saved.',
945
+ ),
946
+ })
947
+ await queryClient.invalidateQueries({
948
+ queryKey: ['ai_assistant', 'agent_settings', 'override', agent.id],
949
+ })
950
+ return
951
+ }
952
+ // Legacy placeholder response: surfaces the Step-4.5 wording for BC.
953
+ setSaveState({
954
+ kind: 'success',
955
+ version: 0,
956
+ message:
957
+ payload.message ??
958
+ t(
959
+ 'ai_assistant.agents.prompt.pendingMessage',
960
+ 'Prompt overrides accepted.',
961
+ ),
962
+ })
963
+ } catch (err) {
964
+ setSaveState({
965
+ kind: 'error',
966
+ message: err instanceof Error ? err.message : String(err),
967
+ })
968
+ } finally {
969
+ setIsSaving(false)
970
+ }
971
+ }, [activeOverrides, agent.id, isSaving, queryClient, t])
972
+
973
+ return (
974
+ <div className="flex flex-col gap-4" data-ai-agent-detail={agent.id}>
975
+ <section className="rounded-lg border border-border bg-background p-4">
976
+ <h2 className="text-xl font-semibold">{agent.label}</h2>
977
+ <p className="mt-1 text-sm text-muted-foreground">{agent.description}</p>
978
+ <dl className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 text-sm">
979
+ <div>
980
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
981
+ {t('ai_assistant.agents.meta.module', 'Module')}
982
+ </dt>
983
+ <dd className="mt-1 font-mono text-xs">{agent.moduleId}</dd>
984
+ </div>
985
+ <div>
986
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
987
+ {t('ai_assistant.agents.meta.id', 'Agent id')}
988
+ </dt>
989
+ <dd className="mt-1 font-mono text-xs">{agent.id}</dd>
990
+ </div>
991
+ <div>
992
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
993
+ {t('ai_assistant.agents.meta.executionMode', 'Execution mode')}
994
+ </dt>
995
+ <dd className="mt-1">
996
+ <StatusBadge variant={executionModeStatusMap[agent.executionMode]}>
997
+ {agent.executionMode}
998
+ </StatusBadge>
999
+ </dd>
1000
+ </div>
1001
+ <div>
1002
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
1003
+ {t('ai_assistant.agents.meta.mutationPolicy', 'Mutation policy')}
1004
+ </dt>
1005
+ <dd className="mt-1">
1006
+ <StatusBadge
1007
+ variant={mutationPolicyStatusMap[agent.mutationPolicy] ?? 'neutral'}
1008
+ dot
1009
+ >
1010
+ {agent.mutationPolicy}
1011
+ </StatusBadge>
1012
+ </dd>
1013
+ </div>
1014
+ <div>
1015
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
1016
+ {t('ai_assistant.agents.meta.readOnly', 'Read-only')}
1017
+ </dt>
1018
+ <dd className="mt-1 text-xs">
1019
+ {agent.readOnly
1020
+ ? t('ai_assistant.agents.meta.readOnlyYes', 'Yes')
1021
+ : t('ai_assistant.agents.meta.readOnlyNo', 'No')}
1022
+ </dd>
1023
+ </div>
1024
+ <div>
1025
+ <dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
1026
+ {t('ai_assistant.agents.meta.maxSteps', 'Max steps')}
1027
+ </dt>
1028
+ <dd className="mt-1 font-mono text-xs">
1029
+ {agent.maxSteps ?? t('ai_assistant.agents.meta.unlimited', 'Unlimited')}
1030
+ </dd>
1031
+ </div>
1032
+ </dl>
1033
+ </section>
1034
+
1035
+ <MutationPolicySection agent={agent} />
1036
+
1037
+ <section
1038
+ className="rounded-lg border border-border bg-background p-4"
1039
+ data-ai-agent-prompt-editor={agent.id}
1040
+ >
1041
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-3">
1042
+ <div>
1043
+ <h3 className="text-sm font-semibold">
1044
+ {t('ai_assistant.agents.prompt.title', 'Prompt sections')}
1045
+ </h3>
1046
+ <p className="text-xs text-muted-foreground">
1047
+ {t(
1048
+ 'ai_assistant.agents.prompt.subtitle',
1049
+ 'Toggle any section to write an additive override. Saving stores a new tenant-scoped version; built-in section text is always preserved.',
1050
+ )}
1051
+ </p>
1052
+ </div>
1053
+ <Button
1054
+ type="button"
1055
+ size="sm"
1056
+ onClick={() => void handleSave()}
1057
+ disabled={isSaving || !hasAnyOverride}
1058
+ data-ai-agent-prompt-save
1059
+ >
1060
+ {isSaving ? (
1061
+ <Loader2 className="size-4 animate-spin" aria-hidden />
1062
+ ) : (
1063
+ <Save className="size-4" aria-hidden />
1064
+ )}
1065
+ <span>{t('ai_assistant.agents.prompt.save', 'Save overrides')}</span>
1066
+ </Button>
1067
+ </header>
1068
+ <div className="mt-3">
1069
+ <Alert variant="info" data-ai-agent-prompt-notice>
1070
+ <Wand2 className="size-4" aria-hidden />
1071
+ <AlertTitle>
1072
+ {t('ai_assistant.agents.override.noticeTitle', 'Prompt overrides are additive')}
1073
+ </AlertTitle>
1074
+ <AlertDescription>
1075
+ {t(
1076
+ 'ai_assistant.agents.override.noticeBody',
1077
+ 'Overrides append to the built-in sections — they never remove or replace shipped text. Saved versions are tenant-scoped and auditable from the history panel below.',
1078
+ )}
1079
+ </AlertDescription>
1080
+ </Alert>
1081
+ </div>
1082
+ {saveState.kind === 'success' ? (
1083
+ <Alert variant="success" className="mt-3" data-ai-agent-prompt-state="success">
1084
+ <CheckCircle2 className="size-4" aria-hidden />
1085
+ <AlertTitle>
1086
+ {saveState.version > 0
1087
+ ? t('ai_assistant.agents.override.savedTitle', 'Prompt override saved')
1088
+ : t('ai_assistant.agents.prompt.pendingTitle', 'Overrides accepted')}
1089
+ </AlertTitle>
1090
+ <AlertDescription>
1091
+ {saveState.version > 0
1092
+ ? `${saveState.message} ${t('ai_assistant.agents.override.versionLabel', 'Version')} ${saveState.version}.`
1093
+ : saveState.message}
1094
+ </AlertDescription>
1095
+ </Alert>
1096
+ ) : null}
1097
+ {saveState.kind === 'error' ? (
1098
+ <Alert variant="destructive" className="mt-3" data-ai-agent-prompt-state="error">
1099
+ <AlertCircle className="size-4" aria-hidden />
1100
+ <AlertTitle>
1101
+ {t('ai_assistant.agents.override.errorTitle', 'Failed to save prompt override')}
1102
+ </AlertTitle>
1103
+ <AlertDescription>{saveState.message}</AlertDescription>
1104
+ </Alert>
1105
+ ) : null}
1106
+ <div className="mt-3 flex flex-col gap-3">
1107
+ <div>
1108
+ <span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
1109
+ {t('ai_assistant.agents.prompt.fullSystemPromptLabel', 'Full system prompt (default)')}
1110
+ </span>
1111
+ <pre className="mt-2 max-h-40 overflow-auto rounded border border-border bg-muted/30 p-2 text-xs font-mono whitespace-pre-wrap">
1112
+ {agent.systemPrompt}
1113
+ </pre>
1114
+ </div>
1115
+ {PROMPT_SECTION_IDS.map((sectionId) => (
1116
+ <PromptSectionEditor
1117
+ key={sectionId}
1118
+ sectionId={sectionId}
1119
+ defaultText={
1120
+ sectionId === 'role'
1121
+ ? agent.systemPrompt
1122
+ : t(
1123
+ 'ai_assistant.agents.prompt.defaultSectionPlaceholder',
1124
+ 'No default copy declared for this section — the agent ships a single systemPrompt. Override to inject additional text once Step 5.3 lands.',
1125
+ )
1126
+ }
1127
+ overrideText={overrideDrafts[sectionId]}
1128
+ override={overrideFlags[sectionId]}
1129
+ onToggleOverride={(next) => {
1130
+ setOverrideFlags((prev) => ({ ...prev, [sectionId]: next }))
1131
+ if (next) {
1132
+ setOverrideDrafts((prev) => {
1133
+ if (prev[sectionId]) return prev
1134
+ const defaultText =
1135
+ sectionId === 'role'
1136
+ ? agent.systemPrompt
1137
+ : ''
1138
+ return { ...prev, [sectionId]: defaultText }
1139
+ })
1140
+ }
1141
+ }}
1142
+ onOverrideChange={(next) =>
1143
+ setOverrideDrafts((prev) => ({ ...prev, [sectionId]: next }))
1144
+ }
1145
+ onSaveShortcut={() => void handleSave()}
1146
+ />
1147
+ ))}
1148
+ </div>
1149
+ </section>
1150
+
1151
+ <section
1152
+ className="rounded-lg border border-border bg-background p-4"
1153
+ data-ai-agent-tools-list={agent.id}
1154
+ >
1155
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-3">
1156
+ <div>
1157
+ <h3 className="text-sm font-semibold">
1158
+ {t('ai_assistant.agents.tools.title', 'Allowed tools')}
1159
+ </h3>
1160
+ <p className="text-xs text-muted-foreground">
1161
+ {t(
1162
+ 'ai_assistant.agents.tools.subtitle',
1163
+ 'Read-only surface in Phase 2. Editing the per-tool toggle and mutation policy lands in Step 5.4 / Phase 3.',
1164
+ )}
1165
+ </p>
1166
+ </div>
1167
+ <Badge variant="neutral" className="font-mono text-xs">
1168
+ {agent.tools.length}
1169
+ </Badge>
1170
+ </header>
1171
+ <div className="mt-3 flex flex-col gap-2">
1172
+ {agent.tools.length === 0 ? (
1173
+ <p className="text-xs text-muted-foreground">
1174
+ {t(
1175
+ 'ai_assistant.agents.tools.emptyBody',
1176
+ 'This agent declares no tools in its allowedTools whitelist.',
1177
+ )}
1178
+ </p>
1179
+ ) : (
1180
+ agent.tools.map((tool) => <ToolRow key={tool.name} tool={tool} />)
1181
+ )}
1182
+ </div>
1183
+ </section>
1184
+
1185
+ <section
1186
+ className="rounded-lg border border-border bg-background p-4"
1187
+ data-ai-agent-override-history={agent.id}
1188
+ >
1189
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-3">
1190
+ <div className="flex items-center gap-2">
1191
+ <History className="size-4 text-muted-foreground" aria-hidden />
1192
+ <div>
1193
+ <h3 className="text-sm font-semibold">
1194
+ {t('ai_assistant.agents.override.history.title', 'Prompt override history')}
1195
+ </h3>
1196
+ <p className="text-xs text-muted-foreground">
1197
+ {t(
1198
+ 'ai_assistant.agents.override.history.subtitle',
1199
+ 'Newest first. Each save creates a new version scoped to the current tenant.',
1200
+ )}
1201
+ </p>
1202
+ </div>
1203
+ </div>
1204
+ {overrideQuery.data ? (
1205
+ <Badge variant="neutral" className="font-mono text-xs">
1206
+ {overrideQuery.data.versions.length}
1207
+ </Badge>
1208
+ ) : null}
1209
+ </header>
1210
+ <div className="mt-3 flex flex-col gap-2">
1211
+ {overrideQuery.isLoading ? (
1212
+ <SettingsLoading
1213
+ message={t(
1214
+ 'ai_assistant.agents.override.history.loading',
1215
+ 'Loading override history...',
1216
+ )}
1217
+ />
1218
+ ) : overrideQuery.isError ? (
1219
+ <Alert variant="destructive" data-ai-agent-override-history-error>
1220
+ <AlertCircle className="size-4" aria-hidden />
1221
+ <AlertTitle>
1222
+ {t(
1223
+ 'ai_assistant.agents.override.history.errorTitle',
1224
+ 'Failed to load override history',
1225
+ )}
1226
+ </AlertTitle>
1227
+ <AlertDescription>
1228
+ {overrideQuery.error instanceof Error
1229
+ ? overrideQuery.error.message
1230
+ : String(overrideQuery.error)}
1231
+ </AlertDescription>
1232
+ </Alert>
1233
+ ) : (overrideQuery.data?.versions ?? []).length === 0 ? (
1234
+ <p
1235
+ className="text-xs text-muted-foreground"
1236
+ data-ai-agent-override-history-empty
1237
+ >
1238
+ {t(
1239
+ 'ai_assistant.agents.override.history.empty',
1240
+ 'No prompt overrides have been saved for this agent yet.',
1241
+ )}
1242
+ </p>
1243
+ ) : (
1244
+ (overrideQuery.data?.versions ?? []).slice(0, 5).map((entry) => (
1245
+ <div
1246
+ key={entry.id}
1247
+ className="flex items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
1248
+ data-ai-agent-override-history-row={entry.version}
1249
+ >
1250
+ <div className="flex flex-col min-w-0">
1251
+ <span className="text-xs font-semibold">
1252
+ {t('ai_assistant.agents.override.versionLabel', 'Version')} {entry.version}
1253
+ </span>
1254
+ <span className="truncate text-xs text-muted-foreground">
1255
+ {new Date(entry.updatedAt).toLocaleString()}
1256
+ </span>
1257
+ </div>
1258
+ <Badge variant="neutral" className="font-mono text-xs">
1259
+ {Object.keys(entry.sections ?? {}).length}{' '}
1260
+ {t('ai_assistant.agents.override.history.sectionsLabel', 'sections')}
1261
+ </Badge>
1262
+ </div>
1263
+ ))
1264
+ )}
1265
+ </div>
1266
+ </section>
1267
+
1268
+ <section
1269
+ className="rounded-lg border border-border bg-background p-4"
1270
+ data-ai-agent-attachments={agent.id}
1271
+ >
1272
+ <header className="flex items-center justify-between gap-3 border-b border-border pb-3">
1273
+ <div>
1274
+ <h3 className="text-sm font-semibold">
1275
+ {t('ai_assistant.agents.attachments.title', 'Attachment policy')}
1276
+ </h3>
1277
+ <p className="text-xs text-muted-foreground">
1278
+ {t(
1279
+ 'ai_assistant.agents.attachments.subtitle',
1280
+ 'Accepted media types the agent declares. Read-only in Phase 2.',
1281
+ )}
1282
+ </p>
1283
+ </div>
1284
+ </header>
1285
+ <div className="mt-3">
1286
+ <AttachmentPolicyBadges mediaTypes={agent.acceptedMediaTypes} />
1287
+ </div>
1288
+ </section>
1289
+ </div>
1290
+ )
1291
+ }
1292
+
1293
+ export function AiAgentSettingsPageClient() {
1294
+ const t = useT()
1295
+ const [selectedAgentId, setSelectedAgentId] = React.useState<string | null>(null)
1296
+
1297
+ const { data, isLoading, isError, error, refetch, isFetching } = useQuery<AgentsResponse>({
1298
+ queryKey: ['ai_assistant', 'agent_settings', 'agents'],
1299
+ queryFn: fetchAgents,
1300
+ })
1301
+
1302
+ const agents = React.useMemo<AgentSettings[]>(() => data?.agents ?? [], [data])
1303
+
1304
+ React.useEffect(() => {
1305
+ if (!agents.length) {
1306
+ if (selectedAgentId !== null) setSelectedAgentId(null)
1307
+ return
1308
+ }
1309
+ if (!selectedAgentId || !agents.some((agent) => agent.id === selectedAgentId)) {
1310
+ setSelectedAgentId(agents[0].id)
1311
+ }
1312
+ }, [agents, selectedAgentId])
1313
+
1314
+ const selectedAgent = React.useMemo<AgentSettings | null>(() => {
1315
+ if (!selectedAgentId) return null
1316
+ return agents.find((agent) => agent.id === selectedAgentId) ?? null
1317
+ }, [agents, selectedAgentId])
1318
+
1319
+ if (isLoading) {
1320
+ return (
1321
+ <SettingsLoading
1322
+ message={t('ai_assistant.agents.loadingAgents', 'Loading AI agents...')}
1323
+ />
1324
+ )
1325
+ }
1326
+
1327
+ if (isError) {
1328
+ return (
1329
+ <Alert variant="destructive" data-ai-agent-settings-error>
1330
+ <AlertCircle className="size-4" aria-hidden />
1331
+ <AlertTitle>
1332
+ {t('ai_assistant.agents.loadErrorTitle', 'Failed to load AI agents')}
1333
+ </AlertTitle>
1334
+ <AlertDescription>
1335
+ <span>{error instanceof Error ? error.message : String(error)}</span>
1336
+ <div className="mt-2">
1337
+ <Button
1338
+ type="button"
1339
+ size="sm"
1340
+ variant="outline"
1341
+ onClick={() => {
1342
+ void refetch()
1343
+ }}
1344
+ >
1345
+ <RefreshCcw className="size-4" aria-hidden />
1346
+ <span>{t('ai_assistant.agents.retry', 'Retry')}</span>
1347
+ </Button>
1348
+ </div>
1349
+ </AlertDescription>
1350
+ </Alert>
1351
+ )
1352
+ }
1353
+
1354
+ if (!agents.length) {
1355
+ return <EmptyAgents />
1356
+ }
1357
+
1358
+ return (
1359
+ <TooltipProvider delayDuration={200}>
1360
+ <div className="flex flex-col gap-4" data-ai-agent-settings>
1361
+ <header className="flex flex-col gap-1">
1362
+ <h1 className="text-2xl font-bold tracking-tight">
1363
+ {t('ai_assistant.agents.title', 'AI Agents')}
1364
+ </h1>
1365
+ <p className="text-sm text-muted-foreground">
1366
+ {t(
1367
+ 'ai_assistant.agents.subtitle',
1368
+ 'Inspect every registered agent and manage tenant-scoped additive prompt-section overrides.',
1369
+ )}
1370
+ </p>
1371
+ </header>
1372
+
1373
+ <section
1374
+ className="flex flex-col gap-3 rounded-lg border border-border bg-background p-3"
1375
+ data-ai-agent-settings-picker-wrap
1376
+ >
1377
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
1378
+ <div className="flex flex-col gap-2 sm:flex-1">
1379
+ <Label htmlFor="ai-agent-settings-picker">
1380
+ {t('ai_assistant.agents.agentPickerLabel', 'Agent')}
1381
+ </Label>
1382
+ <select
1383
+ id="ai-agent-settings-picker"
1384
+ data-ai-agent-settings-picker
1385
+ className="h-9 rounded-md border border-input bg-background px-3 text-sm"
1386
+ value={selectedAgentId ?? ''}
1387
+ onChange={(event) => setSelectedAgentId(event.target.value)}
1388
+ >
1389
+ {agents.map((agent) => (
1390
+ <option key={agent.id} value={agent.id}>
1391
+ {agent.label} ({agent.id})
1392
+ </option>
1393
+ ))}
1394
+ </select>
1395
+ </div>
1396
+ <div className="flex items-center gap-2 sm:flex-shrink-0">
1397
+ <IconButton
1398
+ type="button"
1399
+ variant="ghost"
1400
+ size="sm"
1401
+ onClick={() => {
1402
+ void refetch()
1403
+ }}
1404
+ aria-label={t('ai_assistant.agents.refresh', 'Refresh agents')}
1405
+ disabled={isFetching}
1406
+ >
1407
+ <RefreshCcw className="size-4" aria-hidden />
1408
+ </IconButton>
1409
+ </div>
1410
+ </div>
1411
+ </section>
1412
+
1413
+ {selectedAgent ? <AgentDetailPanel agent={selectedAgent} /> : null}
1414
+ </div>
1415
+ </TooltipProvider>
1416
+ )
1417
+ }
1418
+
1419
+ export default AiAgentSettingsPageClient