@open-mercato/ai-assistant 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +82 -18
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +38 -19
- package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +537 -22
- package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +338 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +123 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +90 -1
- package/dist/modules/ai_assistant/i18n/en.json +90 -1
- package/dist/modules/ai_assistant/i18n/es.json +90 -1
- package/dist/modules/ai_assistant/i18n/pl.json +90 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
- package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
- package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +26 -0
- package/jest.config.cjs +2 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
- package/src/modules/ai_assistant/api/route/route.ts +49 -25
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
- package/src/modules/ai_assistant/api/settings/route.ts +721 -27
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
- package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +164 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
- package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/i18n/de.json +90 -1
- package/src/modules/ai_assistant/i18n/en.json +90 -1
- package/src/modules/ai_assistant/i18n/es.json +90 -1
- package/src/modules/ai_assistant/i18n/pl.json +90 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
- package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
- package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
- package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
- package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
- package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260512130000.ts +13 -0
package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx
CHANGED
|
@@ -24,8 +24,17 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
24
24
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
25
25
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
26
26
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
27
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
27
28
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
28
29
|
import { Label } from '@open-mercato/ui/primitives/label'
|
|
30
|
+
import { Radio, RadioGroup } from '@open-mercato/ui/primitives/radio'
|
|
31
|
+
import {
|
|
32
|
+
Select,
|
|
33
|
+
SelectContent,
|
|
34
|
+
SelectItem,
|
|
35
|
+
SelectTrigger,
|
|
36
|
+
SelectValue,
|
|
37
|
+
} from '@open-mercato/ui/primitives/select'
|
|
29
38
|
import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'
|
|
30
39
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
31
40
|
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
@@ -37,11 +46,13 @@ import {
|
|
|
37
46
|
} from '@open-mercato/ui/primitives/tooltip'
|
|
38
47
|
import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
|
|
39
48
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
49
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
50
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
40
51
|
import { useAiShortcuts } from '@open-mercato/ui/ai'
|
|
41
52
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
53
|
+
// The agent picker is deliberately duplicated between the playground and this
|
|
54
|
+
// settings page. Duplicated markup is under the 50-line threshold, so extraction
|
|
55
|
+
// stays deferred per the Step 4.6 brief.
|
|
45
56
|
|
|
46
57
|
type AgentTool = {
|
|
47
58
|
name: string
|
|
@@ -57,6 +68,10 @@ type AgentSettings = {
|
|
|
57
68
|
description: string
|
|
58
69
|
systemPrompt: string
|
|
59
70
|
executionMode: 'chat' | 'object'
|
|
71
|
+
defaultProvider: string | null
|
|
72
|
+
defaultModel: string | null
|
|
73
|
+
defaultBaseUrl: string | null
|
|
74
|
+
allowRuntimeModelOverride: boolean
|
|
60
75
|
mutationPolicy: string
|
|
61
76
|
readOnly: boolean
|
|
62
77
|
maxSteps: number | null
|
|
@@ -72,6 +87,58 @@ type AgentsResponse = {
|
|
|
72
87
|
total: number
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
type ProviderConfig = {
|
|
91
|
+
id: string
|
|
92
|
+
name: string
|
|
93
|
+
defaultModel: string
|
|
94
|
+
configured: boolean
|
|
95
|
+
defaultModels: Array<{ id: string; name: string }>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type AgentResolution = {
|
|
99
|
+
agentId: string
|
|
100
|
+
moduleId: string
|
|
101
|
+
allowRuntimeModelOverride: boolean
|
|
102
|
+
codeDefaultProviderId: string | null
|
|
103
|
+
codeDefaultModelId: string | null
|
|
104
|
+
override: {
|
|
105
|
+
providerId: string | null
|
|
106
|
+
modelId: string | null
|
|
107
|
+
baseURL: string | null
|
|
108
|
+
updatedAt: string
|
|
109
|
+
} | null
|
|
110
|
+
runtimeOverrideAllowlist: {
|
|
111
|
+
env: TenantAllowlist | null
|
|
112
|
+
tenant: TenantAllowlist | null
|
|
113
|
+
effective: EffectiveAllowlist
|
|
114
|
+
envVarNames: {
|
|
115
|
+
providers: string
|
|
116
|
+
modelsByProvider: Record<string, string>
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
providerId: string
|
|
120
|
+
modelId: string
|
|
121
|
+
baseURL: string | null
|
|
122
|
+
source: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type TenantAllowlist = {
|
|
126
|
+
allowedProviders: string[] | null
|
|
127
|
+
allowedModelsByProvider: Record<string, string[]>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type EffectiveAllowlist = {
|
|
131
|
+
providers: string[] | null
|
|
132
|
+
modelsByProvider: Record<string, string[]>
|
|
133
|
+
hasRestrictions: boolean
|
|
134
|
+
tenantOverridesActive: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type RuntimeSettingsResponse = {
|
|
138
|
+
availableProviders: ProviderConfig[]
|
|
139
|
+
agents: AgentResolution[]
|
|
140
|
+
}
|
|
141
|
+
|
|
75
142
|
const PROMPT_SECTION_IDS = [
|
|
76
143
|
'role',
|
|
77
144
|
'scope',
|
|
@@ -112,6 +179,16 @@ async function fetchAgents(): Promise<AgentsResponse> {
|
|
|
112
179
|
return result
|
|
113
180
|
}
|
|
114
181
|
|
|
182
|
+
async function fetchRuntimeSettings(): Promise<RuntimeSettingsResponse> {
|
|
183
|
+
const { result, status } = await apiCallOrThrow<RuntimeSettingsResponse>(
|
|
184
|
+
'/api/ai_assistant/settings',
|
|
185
|
+
{ method: 'GET', credentials: 'include' },
|
|
186
|
+
{ errorMessage: 'Failed to load runtime settings' },
|
|
187
|
+
)
|
|
188
|
+
if (!result) throw new Error(`Failed to load runtime settings (${status})`)
|
|
189
|
+
return result
|
|
190
|
+
}
|
|
191
|
+
|
|
115
192
|
type OverrideVersion = {
|
|
116
193
|
id: string
|
|
117
194
|
agentId: string
|
|
@@ -258,8 +335,8 @@ function PromptSectionEditor({
|
|
|
258
335
|
className="flex flex-col gap-2 rounded-md border border-border bg-muted/20 p-3"
|
|
259
336
|
data-ai-agent-prompt-section={sectionId}
|
|
260
337
|
>
|
|
261
|
-
<div className="flex items-
|
|
262
|
-
<div className="flex flex-col">
|
|
338
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
339
|
+
<div className="flex min-w-0 flex-col">
|
|
263
340
|
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
264
341
|
{sectionLabel}
|
|
265
342
|
</span>
|
|
@@ -320,7 +397,7 @@ function ToolRow({ tool }: { tool: AgentTool }) {
|
|
|
320
397
|
const t = useT()
|
|
321
398
|
return (
|
|
322
399
|
<div
|
|
323
|
-
className="flex items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
|
|
400
|
+
className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
|
|
324
401
|
data-ai-agent-tool-row={tool.name}
|
|
325
402
|
>
|
|
326
403
|
<div className="flex items-start gap-2 min-w-0">
|
|
@@ -330,7 +407,7 @@ function ToolRow({ tool }: { tool: AgentTool }) {
|
|
|
330
407
|
<span className="truncate text-xs font-mono text-muted-foreground">{tool.name}</span>
|
|
331
408
|
</div>
|
|
332
409
|
</div>
|
|
333
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
410
|
+
<div className="flex flex-wrap items-center gap-2 flex-shrink-0">
|
|
334
411
|
{tool.isMutation ? (
|
|
335
412
|
<StatusBadge variant="warning" dot>
|
|
336
413
|
{t('ai_assistant.agents.tools.mutationBadge', 'Mutation')}
|
|
@@ -425,6 +502,12 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
425
502
|
| { kind: 'success'; message: string }
|
|
426
503
|
| { kind: 'error'; message: string }
|
|
427
504
|
>({ kind: 'idle' })
|
|
505
|
+
const { runMutation: runSavePolicyMutation } = useGuardedMutation({
|
|
506
|
+
contextId: `ai-agent-mutation-policy-save-${agent.id}`,
|
|
507
|
+
})
|
|
508
|
+
const { runMutation: runClearPolicyMutation } = useGuardedMutation({
|
|
509
|
+
contextId: `ai-agent-mutation-policy-clear-${agent.id}`,
|
|
510
|
+
})
|
|
428
511
|
|
|
429
512
|
React.useEffect(() => {
|
|
430
513
|
setSelected(currentOverride?.mutationPolicy ?? null)
|
|
@@ -445,93 +528,94 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
445
528
|
setIsSaving(true)
|
|
446
529
|
setState({ kind: 'idle' })
|
|
447
530
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
531
|
+
await runSavePolicyMutation({
|
|
532
|
+
operation: async () => {
|
|
533
|
+
const { ok, status, result } = await apiCall<{
|
|
534
|
+
ok?: boolean
|
|
535
|
+
error?: string
|
|
536
|
+
code?: string
|
|
537
|
+
codeDeclared?: MutationPolicy
|
|
538
|
+
requested?: MutationPolicy
|
|
539
|
+
}>(
|
|
540
|
+
`/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/mutation-policy`,
|
|
541
|
+
{
|
|
542
|
+
method: 'POST',
|
|
543
|
+
headers: { 'content-type': 'application/json' },
|
|
544
|
+
credentials: 'include',
|
|
545
|
+
body: JSON.stringify({ mutationPolicy: selected }),
|
|
546
|
+
},
|
|
547
|
+
)
|
|
548
|
+
const payload = result ?? {}
|
|
549
|
+
if (!ok) {
|
|
550
|
+
const message =
|
|
551
|
+
payload.code === 'escalation_not_allowed'
|
|
552
|
+
? (payload.error ??
|
|
553
|
+
t(
|
|
554
|
+
'ai_assistant.agents.mutation_policy.errors.escalationNotAllowed',
|
|
555
|
+
'Cannot upgrade beyond the agent\'s declared policy — this is a code-level change.',
|
|
556
|
+
))
|
|
557
|
+
: (payload.error ??
|
|
558
|
+
`Failed to save mutation policy (${status}).`)
|
|
559
|
+
throw new Error(message)
|
|
560
|
+
}
|
|
461
561
|
},
|
|
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
|
-
),
|
|
562
|
+
context: {},
|
|
483
563
|
})
|
|
564
|
+
const successMessage = t(
|
|
565
|
+
'ai_assistant.agents.mutation_policy.savedMessage',
|
|
566
|
+
'Mutation policy override saved.',
|
|
567
|
+
)
|
|
568
|
+
setState({ kind: 'success', message: successMessage })
|
|
569
|
+
flash(successMessage, 'success')
|
|
484
570
|
await queryClient.invalidateQueries({
|
|
485
571
|
queryKey: ['ai_assistant', 'agent_settings', 'mutation_policy', agent.id],
|
|
486
572
|
})
|
|
487
573
|
} catch (err) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
})
|
|
574
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
575
|
+
setState({ kind: 'error', message })
|
|
576
|
+
flash(message, 'error')
|
|
492
577
|
} finally {
|
|
493
578
|
setIsSaving(false)
|
|
494
579
|
}
|
|
495
|
-
}, [agent.id, isSaving, queryClient, selected, t])
|
|
580
|
+
}, [agent.id, isSaving, queryClient, runSavePolicyMutation, selected, t])
|
|
496
581
|
|
|
497
582
|
const clear = React.useCallback(async () => {
|
|
498
583
|
if (isClearing) return
|
|
499
584
|
setIsClearing(true)
|
|
500
585
|
setState({ kind: 'idle' })
|
|
501
586
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
587
|
+
await runClearPolicyMutation({
|
|
588
|
+
operation: async () => {
|
|
589
|
+
const { ok, status, result } = await apiCall<{
|
|
590
|
+
ok?: boolean
|
|
591
|
+
error?: string
|
|
592
|
+
}>(
|
|
593
|
+
`/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/mutation-policy`,
|
|
594
|
+
{
|
|
595
|
+
method: 'DELETE',
|
|
596
|
+
credentials: 'include',
|
|
597
|
+
},
|
|
598
|
+
)
|
|
599
|
+
const payload = result ?? {}
|
|
600
|
+
if (!ok) {
|
|
601
|
+
throw new Error(payload.error ?? `Failed to clear override (${status}).`)
|
|
602
|
+
}
|
|
510
603
|
},
|
|
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
|
-
),
|
|
604
|
+
context: {},
|
|
526
605
|
})
|
|
606
|
+
const successMessage = t(
|
|
607
|
+
'ai_assistant.agents.mutation_policy.clearedMessage',
|
|
608
|
+
'Mutation policy override cleared; agent is using its code-declared policy.',
|
|
609
|
+
)
|
|
610
|
+
setState({ kind: 'success', message: successMessage })
|
|
611
|
+
flash(successMessage, 'success')
|
|
527
612
|
await queryClient.invalidateQueries({
|
|
528
613
|
queryKey: ['ai_assistant', 'agent_settings', 'mutation_policy', agent.id],
|
|
529
614
|
})
|
|
530
615
|
} catch (err) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
})
|
|
616
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
617
|
+
setState({ kind: 'error', message })
|
|
618
|
+
flash(message, 'error')
|
|
535
619
|
} finally {
|
|
536
620
|
setIsClearing(false)
|
|
537
621
|
}
|
|
@@ -542,10 +626,10 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
542
626
|
className="rounded-lg border border-border bg-background p-4"
|
|
543
627
|
data-ai-agent-mutation-policy={agent.id}
|
|
544
628
|
>
|
|
545
|
-
<header className="flex items-
|
|
546
|
-
<div className="flex items-
|
|
629
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
630
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
547
631
|
<ShieldAlert className="size-4 text-muted-foreground" aria-hidden />
|
|
548
|
-
<div>
|
|
632
|
+
<div className="min-w-0">
|
|
549
633
|
<h3 className="text-sm font-semibold">
|
|
550
634
|
{t('ai_assistant.agents.mutation_policy.title', 'Mutation policy')}
|
|
551
635
|
</h3>
|
|
@@ -560,6 +644,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
560
644
|
<StatusBadge
|
|
561
645
|
variant={mutationPolicyStatusMap[effectivePolicy] ?? 'neutral'}
|
|
562
646
|
dot
|
|
647
|
+
className="max-w-full whitespace-normal break-all"
|
|
563
648
|
data-ai-agent-mutation-policy-effective
|
|
564
649
|
>
|
|
565
650
|
{effectivePolicy}
|
|
@@ -567,15 +652,16 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
567
652
|
</header>
|
|
568
653
|
|
|
569
654
|
<div className="mt-3 flex flex-col gap-3">
|
|
570
|
-
<div className="grid grid-cols-
|
|
571
|
-
<div>
|
|
655
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] gap-3">
|
|
656
|
+
<div className="min-w-0">
|
|
572
657
|
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
573
658
|
{t('ai_assistant.agents.mutation_policy.codeDeclared', 'Code-declared')}
|
|
574
659
|
</span>
|
|
575
|
-
<div className="mt-1 flex items-center gap-2">
|
|
660
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
576
661
|
<StatusBadge
|
|
577
662
|
variant={mutationPolicyStatusMap[codeDeclared] ?? 'neutral'}
|
|
578
663
|
dot
|
|
664
|
+
className="max-w-full whitespace-normal break-all"
|
|
579
665
|
data-ai-agent-mutation-policy-code-declared
|
|
580
666
|
>
|
|
581
667
|
{codeDeclared}
|
|
@@ -589,7 +675,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
589
675
|
</span>
|
|
590
676
|
</div>
|
|
591
677
|
</div>
|
|
592
|
-
<div>
|
|
678
|
+
<div className="min-w-0">
|
|
593
679
|
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
594
680
|
{t('ai_assistant.agents.mutation_policy.tenantOverride', 'Tenant override')}
|
|
595
681
|
</span>
|
|
@@ -598,6 +684,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
598
684
|
<StatusBadge
|
|
599
685
|
variant={mutationPolicyStatusMap[currentOverride.mutationPolicy] ?? 'neutral'}
|
|
600
686
|
dot
|
|
687
|
+
className="max-w-full whitespace-normal break-all"
|
|
601
688
|
data-ai-agent-mutation-policy-override-current
|
|
602
689
|
>
|
|
603
690
|
{currentOverride.mutationPolicy}
|
|
@@ -654,9 +741,12 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
654
741
|
</AlertDescription>
|
|
655
742
|
</Alert>
|
|
656
743
|
) : (
|
|
657
|
-
<
|
|
744
|
+
<RadioGroup
|
|
745
|
+
value={selected ?? ''}
|
|
746
|
+
onValueChange={(value) => {
|
|
747
|
+
setSelected(value as MutationPolicy)
|
|
748
|
+
}}
|
|
658
749
|
className="flex flex-col gap-2"
|
|
659
|
-
role="radiogroup"
|
|
660
750
|
aria-label={t(
|
|
661
751
|
'ai_assistant.agents.mutation_policy.pickerLabel',
|
|
662
752
|
'Mutation policy override',
|
|
@@ -670,27 +760,24 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
670
760
|
return (
|
|
671
761
|
<Tooltip key={option}>
|
|
672
762
|
<TooltipTrigger asChild>
|
|
673
|
-
<
|
|
763
|
+
<div
|
|
674
764
|
className={`flex items-start gap-3 rounded-md border px-3 py-2 text-sm cursor-pointer transition-colors ${
|
|
675
765
|
wouldEscalate
|
|
676
766
|
? 'border-border bg-muted/30 cursor-not-allowed opacity-60'
|
|
677
767
|
: isSelected
|
|
678
|
-
? 'border-
|
|
768
|
+
? 'border-accent-indigo bg-accent-indigo/5'
|
|
679
769
|
: 'border-border bg-background hover:bg-muted/40'
|
|
680
770
|
}`}
|
|
771
|
+
onClick={() => {
|
|
772
|
+
if (wouldEscalate) return
|
|
773
|
+
setSelected(option)
|
|
774
|
+
}}
|
|
681
775
|
data-ai-agent-mutation-policy-option={option}
|
|
682
776
|
data-ai-agent-mutation-policy-option-disabled={wouldEscalate ? 'true' : 'false'}
|
|
683
777
|
>
|
|
684
|
-
<
|
|
685
|
-
type="radio"
|
|
686
|
-
name={`mutation-policy-${agent.id}`}
|
|
778
|
+
<Radio
|
|
687
779
|
value={option}
|
|
688
|
-
checked={isSelected}
|
|
689
780
|
disabled={wouldEscalate}
|
|
690
|
-
onChange={() => {
|
|
691
|
-
if (wouldEscalate) return
|
|
692
|
-
setSelected(option)
|
|
693
|
-
}}
|
|
694
781
|
className="mt-0.5"
|
|
695
782
|
aria-disabled={wouldEscalate}
|
|
696
783
|
/>
|
|
@@ -713,7 +800,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
713
800
|
)}
|
|
714
801
|
</span>
|
|
715
802
|
</div>
|
|
716
|
-
</
|
|
803
|
+
</div>
|
|
717
804
|
</TooltipTrigger>
|
|
718
805
|
{wouldEscalate ? (
|
|
719
806
|
<TooltipContent>
|
|
@@ -726,7 +813,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
726
813
|
</Tooltip>
|
|
727
814
|
)
|
|
728
815
|
})}
|
|
729
|
-
</
|
|
816
|
+
</RadioGroup>
|
|
730
817
|
)}
|
|
731
818
|
|
|
732
819
|
{state.kind === 'success' ? (
|
|
@@ -751,7 +838,7 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
751
838
|
</Alert>
|
|
752
839
|
) : null}
|
|
753
840
|
|
|
754
|
-
<div className="flex items-center justify-end gap-2">
|
|
841
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
755
842
|
<Button
|
|
756
843
|
type="button"
|
|
757
844
|
size="sm"
|
|
@@ -787,6 +874,580 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
787
874
|
)
|
|
788
875
|
}
|
|
789
876
|
|
|
877
|
+
function AgentModelOverrideSection({ agent }: { agent: AgentSettings }) {
|
|
878
|
+
const t = useT()
|
|
879
|
+
const queryClient = useQueryClient()
|
|
880
|
+
const settingsQuery = useQuery<RuntimeSettingsResponse>({
|
|
881
|
+
queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
|
|
882
|
+
queryFn: fetchRuntimeSettings,
|
|
883
|
+
retry: false,
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
const agentResolution = settingsQuery.data?.agents.find((entry) => entry.agentId === agent.id) ?? null
|
|
887
|
+
const configuredProviders = React.useMemo(
|
|
888
|
+
() => (settingsQuery.data?.availableProviders ?? []).filter((provider) => provider.configured),
|
|
889
|
+
[settingsQuery.data?.availableProviders],
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
const [selectedProviderId, setSelectedProviderId] = React.useState('')
|
|
893
|
+
const [selectedModelId, setSelectedModelId] = React.useState('')
|
|
894
|
+
const [allowedProviders, setAllowedProviders] = React.useState<string[] | null>(null)
|
|
895
|
+
const [allowedModelsByProvider, setAllowedModelsByProvider] = React.useState<Record<string, string[]>>({})
|
|
896
|
+
const [allowlistDirty, setAllowlistDirty] = React.useState(false)
|
|
897
|
+
const [isSaving, setIsSaving] = React.useState(false)
|
|
898
|
+
const [isClearing, setIsClearing] = React.useState(false)
|
|
899
|
+
const [isSavingAllowlist, setIsSavingAllowlist] = React.useState(false)
|
|
900
|
+
const [state, setState] = React.useState<
|
|
901
|
+
| { kind: 'idle' }
|
|
902
|
+
| { kind: 'success'; message: string }
|
|
903
|
+
| { kind: 'error'; message: string }
|
|
904
|
+
>({ kind: 'idle' })
|
|
905
|
+
const { runMutation: runSaveModelOverrideMutation } = useGuardedMutation({
|
|
906
|
+
contextId: `ai-agent-model-override-save-${agent.id}`,
|
|
907
|
+
})
|
|
908
|
+
const { runMutation: runClearModelOverrideMutation } = useGuardedMutation({
|
|
909
|
+
contextId: `ai-agent-model-override-clear-${agent.id}`,
|
|
910
|
+
})
|
|
911
|
+
const { runMutation: runSaveModelAllowlistMutation } = useGuardedMutation({
|
|
912
|
+
contextId: `ai-agent-model-override-allowlist-${agent.id}`,
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
React.useEffect(() => {
|
|
916
|
+
const override = agentResolution?.override
|
|
917
|
+
setSelectedProviderId(override?.providerId ?? '')
|
|
918
|
+
setSelectedModelId(override?.modelId ?? '')
|
|
919
|
+
setAllowedProviders(agentResolution?.runtimeOverrideAllowlist.tenant?.allowedProviders ?? null)
|
|
920
|
+
setAllowedModelsByProvider({
|
|
921
|
+
...(agentResolution?.runtimeOverrideAllowlist.tenant?.allowedModelsByProvider ?? {}),
|
|
922
|
+
})
|
|
923
|
+
setAllowlistDirty(false)
|
|
924
|
+
setState({ kind: 'idle' })
|
|
925
|
+
}, [
|
|
926
|
+
agent.id,
|
|
927
|
+
agentResolution?.override?.modelId,
|
|
928
|
+
agentResolution?.override?.providerId,
|
|
929
|
+
agentResolution?.runtimeOverrideAllowlist.tenant,
|
|
930
|
+
])
|
|
931
|
+
|
|
932
|
+
const selectedProvider = configuredProviders.find((provider) => provider.id === selectedProviderId)
|
|
933
|
+
const isProviderAllowedForPicker = React.useCallback(
|
|
934
|
+
(providerId: string) => {
|
|
935
|
+
if (allowedProviders === null) return true
|
|
936
|
+
return allowedProviders.includes(providerId)
|
|
937
|
+
},
|
|
938
|
+
[allowedProviders],
|
|
939
|
+
)
|
|
940
|
+
const isModelAllowedForPicker = React.useCallback(
|
|
941
|
+
(providerId: string, modelId: string) => {
|
|
942
|
+
const list = allowedModelsByProvider[providerId]
|
|
943
|
+
if (list === undefined) return true
|
|
944
|
+
return list.includes(modelId)
|
|
945
|
+
},
|
|
946
|
+
[allowedModelsByProvider],
|
|
947
|
+
)
|
|
948
|
+
const hasChange =
|
|
949
|
+
selectedProviderId.length > 0 &&
|
|
950
|
+
selectedModelId.length > 0 &&
|
|
951
|
+
(
|
|
952
|
+
selectedProviderId !== (agentResolution?.override?.providerId ?? '') ||
|
|
953
|
+
selectedModelId !== (agentResolution?.override?.modelId ?? '')
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
const save = React.useCallback(async () => {
|
|
957
|
+
if (isSaving || !selectedProviderId || !selectedModelId) return
|
|
958
|
+
setIsSaving(true)
|
|
959
|
+
setState({ kind: 'idle' })
|
|
960
|
+
try {
|
|
961
|
+
await runSaveModelOverrideMutation({
|
|
962
|
+
operation: async () => {
|
|
963
|
+
const { ok, status, result } = await apiCall<{ error?: string; code?: string }>(
|
|
964
|
+
'/api/ai_assistant/settings',
|
|
965
|
+
{
|
|
966
|
+
method: 'PUT',
|
|
967
|
+
headers: { 'content-type': 'application/json' },
|
|
968
|
+
credentials: 'include',
|
|
969
|
+
body: JSON.stringify({
|
|
970
|
+
agentId: agent.id,
|
|
971
|
+
providerId: selectedProviderId,
|
|
972
|
+
modelId: selectedModelId,
|
|
973
|
+
}),
|
|
974
|
+
},
|
|
975
|
+
)
|
|
976
|
+
if (!ok) {
|
|
977
|
+
throw new Error(result?.error ?? `Failed to save model override (${status}).`)
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
context: {},
|
|
981
|
+
})
|
|
982
|
+
const successMessage = t('ai_assistant.agents.model_override.saved', 'Model override saved.')
|
|
983
|
+
setState({ kind: 'success', message: successMessage })
|
|
984
|
+
flash(successMessage, 'success')
|
|
985
|
+
await queryClient.invalidateQueries({
|
|
986
|
+
queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
|
|
987
|
+
})
|
|
988
|
+
} catch (err) {
|
|
989
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
990
|
+
setState({ kind: 'error', message })
|
|
991
|
+
flash(message, 'error')
|
|
992
|
+
} finally {
|
|
993
|
+
setIsSaving(false)
|
|
994
|
+
}
|
|
995
|
+
}, [agent.id, isSaving, queryClient, runSaveModelOverrideMutation, selectedModelId, selectedProviderId, t])
|
|
996
|
+
|
|
997
|
+
const clear = React.useCallback(async () => {
|
|
998
|
+
if (isClearing) return
|
|
999
|
+
setIsClearing(true)
|
|
1000
|
+
setState({ kind: 'idle' })
|
|
1001
|
+
try {
|
|
1002
|
+
await runClearModelOverrideMutation({
|
|
1003
|
+
operation: async () => {
|
|
1004
|
+
const { ok, status, result } = await apiCall<{ error?: string }>(
|
|
1005
|
+
'/api/ai_assistant/settings',
|
|
1006
|
+
{
|
|
1007
|
+
method: 'DELETE',
|
|
1008
|
+
headers: { 'content-type': 'application/json' },
|
|
1009
|
+
credentials: 'include',
|
|
1010
|
+
body: JSON.stringify({ agentId: agent.id }),
|
|
1011
|
+
},
|
|
1012
|
+
)
|
|
1013
|
+
if (!ok) {
|
|
1014
|
+
throw new Error(result?.error ?? `Failed to clear model override (${status}).`)
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
context: {},
|
|
1018
|
+
})
|
|
1019
|
+
setSelectedProviderId('')
|
|
1020
|
+
setSelectedModelId('')
|
|
1021
|
+
const successMessage = t(
|
|
1022
|
+
'ai_assistant.agents.model_override.cleared',
|
|
1023
|
+
'Model override cleared; the agent is using the normal resolution chain.',
|
|
1024
|
+
)
|
|
1025
|
+
setState({ kind: 'success', message: successMessage })
|
|
1026
|
+
flash(successMessage, 'success')
|
|
1027
|
+
await queryClient.invalidateQueries({
|
|
1028
|
+
queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
|
|
1029
|
+
})
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1032
|
+
setState({ kind: 'error', message })
|
|
1033
|
+
flash(message, 'error')
|
|
1034
|
+
} finally {
|
|
1035
|
+
setIsClearing(false)
|
|
1036
|
+
}
|
|
1037
|
+
}, [agent.id, isClearing, queryClient, runClearModelOverrideMutation, t])
|
|
1038
|
+
|
|
1039
|
+
const toggleAllowedProvider = React.useCallback(
|
|
1040
|
+
(providerId: string, next: boolean) => {
|
|
1041
|
+
setAllowlistDirty(true)
|
|
1042
|
+
setState({ kind: 'idle' })
|
|
1043
|
+
setAllowedProviders((current) => {
|
|
1044
|
+
if (next) {
|
|
1045
|
+
return current === null ? [providerId] : Array.from(new Set([...current, providerId]))
|
|
1046
|
+
}
|
|
1047
|
+
const baseline = current === null
|
|
1048
|
+
? configuredProviders.map((provider) => provider.id)
|
|
1049
|
+
: current
|
|
1050
|
+
return baseline.filter((id) => id !== providerId)
|
|
1051
|
+
})
|
|
1052
|
+
},
|
|
1053
|
+
[configuredProviders],
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
const toggleAllowedModel = React.useCallback(
|
|
1057
|
+
(providerId: string, modelId: string, next: boolean) => {
|
|
1058
|
+
setAllowlistDirty(true)
|
|
1059
|
+
setState({ kind: 'idle' })
|
|
1060
|
+
const provider = configuredProviders.find((entry) => entry.id === providerId)
|
|
1061
|
+
const allModelIds = provider?.defaultModels.map((model) => model.id) ?? []
|
|
1062
|
+
setAllowedModelsByProvider((current) => {
|
|
1063
|
+
const existing = current[providerId]
|
|
1064
|
+
const baseline = existing === undefined ? allModelIds : existing
|
|
1065
|
+
const nextModels = next
|
|
1066
|
+
? Array.from(new Set([...baseline, modelId]))
|
|
1067
|
+
: baseline.filter((id) => id !== modelId)
|
|
1068
|
+
return { ...current, [providerId]: nextModels }
|
|
1069
|
+
})
|
|
1070
|
+
},
|
|
1071
|
+
[configuredProviders],
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
const resetAllowlistDraft = React.useCallback(() => {
|
|
1075
|
+
setAllowedProviders(null)
|
|
1076
|
+
setAllowedModelsByProvider({})
|
|
1077
|
+
setAllowlistDirty(true)
|
|
1078
|
+
setState({ kind: 'idle' })
|
|
1079
|
+
}, [])
|
|
1080
|
+
|
|
1081
|
+
const saveAllowlist = React.useCallback(async () => {
|
|
1082
|
+
if (isSavingAllowlist) return
|
|
1083
|
+
setIsSavingAllowlist(true)
|
|
1084
|
+
setState({ kind: 'idle' })
|
|
1085
|
+
try {
|
|
1086
|
+
await runSaveModelAllowlistMutation({
|
|
1087
|
+
operation: async () => {
|
|
1088
|
+
const { ok, status, result } = await apiCall<{ error?: string }>(
|
|
1089
|
+
'/api/ai_assistant/settings',
|
|
1090
|
+
{
|
|
1091
|
+
method: 'PUT',
|
|
1092
|
+
headers: { 'content-type': 'application/json' },
|
|
1093
|
+
credentials: 'include',
|
|
1094
|
+
body: JSON.stringify({
|
|
1095
|
+
agentId: agent.id,
|
|
1096
|
+
allowedOverrideProviders: allowedProviders,
|
|
1097
|
+
allowedOverrideModelsByProvider: allowedModelsByProvider,
|
|
1098
|
+
}),
|
|
1099
|
+
},
|
|
1100
|
+
)
|
|
1101
|
+
if (!ok) {
|
|
1102
|
+
throw new Error(result?.error ?? `Failed to save chat override allowlist (${status}).`)
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
context: {},
|
|
1106
|
+
})
|
|
1107
|
+
setAllowlistDirty(false)
|
|
1108
|
+
const successMessage = t(
|
|
1109
|
+
'ai_assistant.agents.model_override.allowlistSaved',
|
|
1110
|
+
'Chat override choices saved.',
|
|
1111
|
+
)
|
|
1112
|
+
setState({ kind: 'success', message: successMessage })
|
|
1113
|
+
flash(successMessage, 'success')
|
|
1114
|
+
await queryClient.invalidateQueries({
|
|
1115
|
+
queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
|
|
1116
|
+
})
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1119
|
+
setState({ kind: 'error', message })
|
|
1120
|
+
flash(message, 'error')
|
|
1121
|
+
} finally {
|
|
1122
|
+
setIsSavingAllowlist(false)
|
|
1123
|
+
}
|
|
1124
|
+
}, [agent.id, allowedModelsByProvider, allowedProviders, isSavingAllowlist, queryClient, runSaveModelAllowlistMutation, t])
|
|
1125
|
+
|
|
1126
|
+
const busy = isSaving || isClearing || isSavingAllowlist
|
|
1127
|
+
|
|
1128
|
+
return (
|
|
1129
|
+
<section
|
|
1130
|
+
className="rounded-lg border border-border bg-background p-4"
|
|
1131
|
+
data-ai-agent-model-override={agent.id}
|
|
1132
|
+
>
|
|
1133
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
1134
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
1135
|
+
<Bot className="size-4 text-muted-foreground" aria-hidden />
|
|
1136
|
+
<div className="min-w-0">
|
|
1137
|
+
<h3 className="text-sm font-semibold">
|
|
1138
|
+
{t('ai_assistant.agents.model_override.title', 'Provider and model')}
|
|
1139
|
+
</h3>
|
|
1140
|
+
<p className="text-xs text-muted-foreground">
|
|
1141
|
+
{t(
|
|
1142
|
+
'ai_assistant.agents.model_override.subtitle',
|
|
1143
|
+
'Override the default provider and model for this tenant and agent.',
|
|
1144
|
+
)}
|
|
1145
|
+
</p>
|
|
1146
|
+
</div>
|
|
1147
|
+
</div>
|
|
1148
|
+
{agentResolution ? (
|
|
1149
|
+
<StatusBadge
|
|
1150
|
+
variant="info"
|
|
1151
|
+
dot
|
|
1152
|
+
className="max-w-full whitespace-normal break-all"
|
|
1153
|
+
data-ai-agent-model-override-effective
|
|
1154
|
+
>
|
|
1155
|
+
{agentResolution.providerId} / {agentResolution.modelId}
|
|
1156
|
+
</StatusBadge>
|
|
1157
|
+
) : null}
|
|
1158
|
+
</header>
|
|
1159
|
+
|
|
1160
|
+
<div className="mt-3 flex flex-col gap-3">
|
|
1161
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] gap-3">
|
|
1162
|
+
<div className="min-w-0">
|
|
1163
|
+
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1164
|
+
{t('ai_assistant.agents.model_override.codeDefault', 'Code-declared default')}
|
|
1165
|
+
</span>
|
|
1166
|
+
<p className="mt-1 break-all font-mono text-xs">
|
|
1167
|
+
{agent.defaultProvider ?? t('ai_assistant.agents.model_override.anyProvider', 'first configured')}
|
|
1168
|
+
{' / '}
|
|
1169
|
+
{agent.defaultModel ?? t('ai_assistant.agents.model_override.providerDefault', 'provider default')}
|
|
1170
|
+
</p>
|
|
1171
|
+
</div>
|
|
1172
|
+
<div className="min-w-0">
|
|
1173
|
+
<span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1174
|
+
{t('ai_assistant.agents.model_override.tenantOverride', 'Tenant override')}
|
|
1175
|
+
</span>
|
|
1176
|
+
<p className="mt-1 break-all font-mono text-xs text-muted-foreground">
|
|
1177
|
+
{agentResolution?.override
|
|
1178
|
+
? `${agentResolution.override.providerId ?? '—'} / ${agentResolution.override.modelId ?? '—'}`
|
|
1179
|
+
: t('ai_assistant.agents.model_override.noOverride', 'No per-agent override')}
|
|
1180
|
+
</p>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
1184
|
+
{settingsQuery.isLoading ? (
|
|
1185
|
+
<SettingsLoading
|
|
1186
|
+
message={t(
|
|
1187
|
+
'ai_assistant.agents.model_override.loading',
|
|
1188
|
+
'Loading provider catalog...',
|
|
1189
|
+
)}
|
|
1190
|
+
/>
|
|
1191
|
+
) : settingsQuery.isError ? (
|
|
1192
|
+
<Alert variant="destructive" data-ai-agent-model-override-load-error>
|
|
1193
|
+
<AlertCircle className="size-4" aria-hidden />
|
|
1194
|
+
<AlertTitle>
|
|
1195
|
+
{t(
|
|
1196
|
+
'ai_assistant.agents.model_override.loadErrorTitle',
|
|
1197
|
+
'Failed to load provider catalog',
|
|
1198
|
+
)}
|
|
1199
|
+
</AlertTitle>
|
|
1200
|
+
<AlertDescription>
|
|
1201
|
+
{settingsQuery.error instanceof Error
|
|
1202
|
+
? settingsQuery.error.message
|
|
1203
|
+
: String(settingsQuery.error)}
|
|
1204
|
+
</AlertDescription>
|
|
1205
|
+
</Alert>
|
|
1206
|
+
) : (
|
|
1207
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] gap-3">
|
|
1208
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
1209
|
+
<Label htmlFor={`ai-agent-model-provider-${agent.id}`} className="text-xs">
|
|
1210
|
+
{t('ai_assistant.agents.model_override.provider', 'Provider')}
|
|
1211
|
+
</Label>
|
|
1212
|
+
<Select
|
|
1213
|
+
value={selectedProviderId}
|
|
1214
|
+
onValueChange={(value) => {
|
|
1215
|
+
setSelectedProviderId(value)
|
|
1216
|
+
setSelectedModelId('')
|
|
1217
|
+
setState({ kind: 'idle' })
|
|
1218
|
+
}}
|
|
1219
|
+
disabled={busy || configuredProviders.length === 0}
|
|
1220
|
+
>
|
|
1221
|
+
<SelectTrigger
|
|
1222
|
+
id={`ai-agent-model-provider-${agent.id}`}
|
|
1223
|
+
className="w-full min-w-0"
|
|
1224
|
+
data-ai-agent-model-provider-select
|
|
1225
|
+
>
|
|
1226
|
+
<SelectValue
|
|
1227
|
+
placeholder={t(
|
|
1228
|
+
'ai_assistant.agents.model_override.selectProvider',
|
|
1229
|
+
'Select provider',
|
|
1230
|
+
)}
|
|
1231
|
+
/>
|
|
1232
|
+
</SelectTrigger>
|
|
1233
|
+
<SelectContent>
|
|
1234
|
+
{configuredProviders.map((provider) => (
|
|
1235
|
+
<SelectItem key={provider.id} value={provider.id}>
|
|
1236
|
+
{provider.name}
|
|
1237
|
+
</SelectItem>
|
|
1238
|
+
))}
|
|
1239
|
+
</SelectContent>
|
|
1240
|
+
</Select>
|
|
1241
|
+
</div>
|
|
1242
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
1243
|
+
<Label htmlFor={`ai-agent-model-model-${agent.id}`} className="text-xs">
|
|
1244
|
+
{t('ai_assistant.agents.model_override.model', 'Model')}
|
|
1245
|
+
</Label>
|
|
1246
|
+
<Select
|
|
1247
|
+
value={selectedModelId}
|
|
1248
|
+
onValueChange={(value) => {
|
|
1249
|
+
setSelectedModelId(value)
|
|
1250
|
+
setState({ kind: 'idle' })
|
|
1251
|
+
}}
|
|
1252
|
+
disabled={busy || !selectedProvider}
|
|
1253
|
+
>
|
|
1254
|
+
<SelectTrigger
|
|
1255
|
+
id={`ai-agent-model-model-${agent.id}`}
|
|
1256
|
+
className="w-full min-w-0"
|
|
1257
|
+
data-ai-agent-model-select
|
|
1258
|
+
>
|
|
1259
|
+
<SelectValue
|
|
1260
|
+
placeholder={t(
|
|
1261
|
+
'ai_assistant.agents.model_override.selectModel',
|
|
1262
|
+
'Select model',
|
|
1263
|
+
)}
|
|
1264
|
+
/>
|
|
1265
|
+
</SelectTrigger>
|
|
1266
|
+
<SelectContent>
|
|
1267
|
+
{(selectedProvider?.defaultModels ?? []).map((model) => (
|
|
1268
|
+
<SelectItem key={model.id} value={model.id}>
|
|
1269
|
+
{model.name}
|
|
1270
|
+
</SelectItem>
|
|
1271
|
+
))}
|
|
1272
|
+
</SelectContent>
|
|
1273
|
+
</Select>
|
|
1274
|
+
</div>
|
|
1275
|
+
</div>
|
|
1276
|
+
)}
|
|
1277
|
+
|
|
1278
|
+
{agent.allowRuntimeModelOverride ? (
|
|
1279
|
+
<div className="rounded-md border border-border bg-muted/20 p-3" data-ai-agent-model-picker-allowlist>
|
|
1280
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1281
|
+
<div className="min-w-0">
|
|
1282
|
+
<h4 className="text-sm font-semibold">
|
|
1283
|
+
{t(
|
|
1284
|
+
'ai_assistant.agents.model_override.allowlistTitle',
|
|
1285
|
+
'Chat override choices',
|
|
1286
|
+
)}
|
|
1287
|
+
</h4>
|
|
1288
|
+
<p className="text-xs text-muted-foreground">
|
|
1289
|
+
{t(
|
|
1290
|
+
'ai_assistant.agents.model_override.allowlistHelp',
|
|
1291
|
+
'Limit which provider/model overrides users can pick in the chat footer for this agent.',
|
|
1292
|
+
)}
|
|
1293
|
+
</p>
|
|
1294
|
+
{agentResolution?.runtimeOverrideAllowlist.env ? (
|
|
1295
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
1296
|
+
<code className="font-mono text-xs">
|
|
1297
|
+
{agentResolution.runtimeOverrideAllowlist.envVarNames.providers}
|
|
1298
|
+
</code>
|
|
1299
|
+
{' '}
|
|
1300
|
+
{t(
|
|
1301
|
+
'ai_assistant.agents.model_override.envAlsoNarrows',
|
|
1302
|
+
'also narrows this list from env.',
|
|
1303
|
+
)}
|
|
1304
|
+
</p>
|
|
1305
|
+
) : null}
|
|
1306
|
+
</div>
|
|
1307
|
+
<StatusBadge
|
|
1308
|
+
variant={agentResolution?.runtimeOverrideAllowlist.tenant ? 'info' : 'neutral'}
|
|
1309
|
+
dot
|
|
1310
|
+
>
|
|
1311
|
+
{agentResolution?.runtimeOverrideAllowlist.tenant
|
|
1312
|
+
? t('ai_assistant.agents.model_override.allowlistCustom', 'custom')
|
|
1313
|
+
: t('ai_assistant.agents.model_override.allowlistInherited', 'inherited')}
|
|
1314
|
+
</StatusBadge>
|
|
1315
|
+
</div>
|
|
1316
|
+
|
|
1317
|
+
<div className="mt-3 flex flex-col gap-3">
|
|
1318
|
+
{configuredProviders.length === 0 ? (
|
|
1319
|
+
<p className="text-xs text-muted-foreground">
|
|
1320
|
+
{t(
|
|
1321
|
+
'ai_assistant.agents.model_override.allowlistEmpty',
|
|
1322
|
+
'No configured providers are available for chat overrides.',
|
|
1323
|
+
)}
|
|
1324
|
+
</p>
|
|
1325
|
+
) : (
|
|
1326
|
+
configuredProviders.map((provider) => {
|
|
1327
|
+
const providerEnabled = isProviderAllowedForPicker(provider.id)
|
|
1328
|
+
return (
|
|
1329
|
+
<div key={provider.id} className="rounded-md border border-border bg-background p-3">
|
|
1330
|
+
<div className="flex items-center gap-2">
|
|
1331
|
+
<Checkbox
|
|
1332
|
+
id={`ai-agent-picker-provider-${agent.id}-${provider.id}`}
|
|
1333
|
+
checked={providerEnabled}
|
|
1334
|
+
onCheckedChange={(value) =>
|
|
1335
|
+
toggleAllowedProvider(provider.id, value === true)
|
|
1336
|
+
}
|
|
1337
|
+
/>
|
|
1338
|
+
<Label
|
|
1339
|
+
htmlFor={`ai-agent-picker-provider-${agent.id}-${provider.id}`}
|
|
1340
|
+
className="text-sm font-medium"
|
|
1341
|
+
>
|
|
1342
|
+
{provider.name}
|
|
1343
|
+
</Label>
|
|
1344
|
+
</div>
|
|
1345
|
+
{providerEnabled ? (
|
|
1346
|
+
<div className="mt-2 grid grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] gap-2">
|
|
1347
|
+
{provider.defaultModels.map((model) => (
|
|
1348
|
+
<label
|
|
1349
|
+
key={model.id}
|
|
1350
|
+
className="flex min-w-0 items-center gap-2 text-xs"
|
|
1351
|
+
>
|
|
1352
|
+
<Checkbox
|
|
1353
|
+
checked={isModelAllowedForPicker(provider.id, model.id)}
|
|
1354
|
+
onCheckedChange={(value) =>
|
|
1355
|
+
toggleAllowedModel(provider.id, model.id, value === true)
|
|
1356
|
+
}
|
|
1357
|
+
/>
|
|
1358
|
+
<span className="truncate font-mono">{model.id}</span>
|
|
1359
|
+
{model.id === provider.defaultModel ? (
|
|
1360
|
+
<Badge variant="outline" className="text-overline">
|
|
1361
|
+
{t('ai_assistant.agents.model_override.defaultBadge', 'default')}
|
|
1362
|
+
</Badge>
|
|
1363
|
+
) : null}
|
|
1364
|
+
</label>
|
|
1365
|
+
))}
|
|
1366
|
+
</div>
|
|
1367
|
+
) : null}
|
|
1368
|
+
</div>
|
|
1369
|
+
)
|
|
1370
|
+
})
|
|
1371
|
+
)}
|
|
1372
|
+
</div>
|
|
1373
|
+
|
|
1374
|
+
<div className="mt-3 flex flex-wrap items-center justify-end gap-2">
|
|
1375
|
+
<Button
|
|
1376
|
+
type="button"
|
|
1377
|
+
size="sm"
|
|
1378
|
+
variant="outline"
|
|
1379
|
+
onClick={resetAllowlistDraft}
|
|
1380
|
+
disabled={busy}
|
|
1381
|
+
>
|
|
1382
|
+
{t('ai_assistant.agents.model_override.allowlistReset', 'Inherit')}
|
|
1383
|
+
</Button>
|
|
1384
|
+
<Button
|
|
1385
|
+
type="button"
|
|
1386
|
+
size="sm"
|
|
1387
|
+
onClick={() => void saveAllowlist()}
|
|
1388
|
+
disabled={busy || !allowlistDirty}
|
|
1389
|
+
data-ai-agent-model-allowlist-save
|
|
1390
|
+
>
|
|
1391
|
+
{isSavingAllowlist ? (
|
|
1392
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
1393
|
+
) : (
|
|
1394
|
+
<Save className="size-4" aria-hidden />
|
|
1395
|
+
)}
|
|
1396
|
+
<span>{t('ai_assistant.agents.model_override.allowlistSave', 'Save choices')}</span>
|
|
1397
|
+
</Button>
|
|
1398
|
+
</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
) : null}
|
|
1401
|
+
|
|
1402
|
+
{state.kind === 'success' ? (
|
|
1403
|
+
<Alert variant="success" data-ai-agent-model-override-state="success">
|
|
1404
|
+
<CheckCircle2 className="size-4" aria-hidden />
|
|
1405
|
+
<AlertDescription>{state.message}</AlertDescription>
|
|
1406
|
+
</Alert>
|
|
1407
|
+
) : null}
|
|
1408
|
+
{state.kind === 'error' ? (
|
|
1409
|
+
<Alert variant="destructive" data-ai-agent-model-override-state="error">
|
|
1410
|
+
<AlertCircle className="size-4" aria-hidden />
|
|
1411
|
+
<AlertDescription>{state.message}</AlertDescription>
|
|
1412
|
+
</Alert>
|
|
1413
|
+
) : null}
|
|
1414
|
+
|
|
1415
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
1416
|
+
<Button
|
|
1417
|
+
type="button"
|
|
1418
|
+
size="sm"
|
|
1419
|
+
variant="outline"
|
|
1420
|
+
onClick={() => void clear()}
|
|
1421
|
+
disabled={busy || !agentResolution?.override}
|
|
1422
|
+
data-ai-agent-model-clear
|
|
1423
|
+
>
|
|
1424
|
+
{isClearing ? (
|
|
1425
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
1426
|
+
) : (
|
|
1427
|
+
<Trash2 className="size-4" aria-hidden />
|
|
1428
|
+
)}
|
|
1429
|
+
<span>{t('ai_assistant.agents.model_override.clear', 'Clear override')}</span>
|
|
1430
|
+
</Button>
|
|
1431
|
+
<Button
|
|
1432
|
+
type="button"
|
|
1433
|
+
size="sm"
|
|
1434
|
+
onClick={() => void save()}
|
|
1435
|
+
disabled={busy || !hasChange}
|
|
1436
|
+
data-ai-agent-model-save
|
|
1437
|
+
>
|
|
1438
|
+
{isSaving ? (
|
|
1439
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
1440
|
+
) : (
|
|
1441
|
+
<Save className="size-4" aria-hidden />
|
|
1442
|
+
)}
|
|
1443
|
+
<span>{t('ai_assistant.agents.model_override.save', 'Save override')}</span>
|
|
1444
|
+
</Button>
|
|
1445
|
+
</div>
|
|
1446
|
+
</div>
|
|
1447
|
+
</section>
|
|
1448
|
+
)
|
|
1449
|
+
}
|
|
1450
|
+
|
|
790
1451
|
function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
791
1452
|
const t = useT()
|
|
792
1453
|
const queryClient = useQueryClient()
|
|
@@ -816,6 +1477,9 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
816
1477
|
| { kind: 'success'; message: string; version: number }
|
|
817
1478
|
| { kind: 'error'; message: string }
|
|
818
1479
|
>({ kind: 'idle' })
|
|
1480
|
+
const { runMutation: runSavePromptOverrideMutation } = useGuardedMutation({
|
|
1481
|
+
contextId: `ai-agent-prompt-override-save-${agent.id}`,
|
|
1482
|
+
})
|
|
819
1483
|
|
|
820
1484
|
const overrideQuery = useQuery<OverrideResponse>({
|
|
821
1485
|
queryKey: ['ai_assistant', 'agent_settings', 'override', agent.id],
|
|
@@ -901,94 +1565,102 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
901
1565
|
setIsSaving(true)
|
|
902
1566
|
setSaveState({ kind: 'idle' })
|
|
903
1567
|
try {
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1568
|
+
const payload = await runSavePromptOverrideMutation({
|
|
1569
|
+
operation: async () => {
|
|
1570
|
+
const { ok, status, result } = await apiCall<{
|
|
1571
|
+
ok?: boolean
|
|
1572
|
+
pending?: boolean
|
|
1573
|
+
version?: number
|
|
1574
|
+
updatedAt?: string
|
|
1575
|
+
message?: string
|
|
1576
|
+
error?: string
|
|
1577
|
+
code?: string
|
|
1578
|
+
reservedKeys?: string[]
|
|
1579
|
+
}>(
|
|
1580
|
+
`/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/prompt-override`,
|
|
1581
|
+
{
|
|
1582
|
+
method: 'POST',
|
|
1583
|
+
headers: { 'content-type': 'application/json' },
|
|
1584
|
+
credentials: 'include',
|
|
1585
|
+
body: JSON.stringify({
|
|
1586
|
+
// Send both keys so a pre-Step-5.3 server still accepts the payload.
|
|
1587
|
+
sections: activeOverrides,
|
|
1588
|
+
overrides: activeOverrides,
|
|
1589
|
+
}),
|
|
1590
|
+
},
|
|
1591
|
+
)
|
|
1592
|
+
const body = result ?? {}
|
|
1593
|
+
if (!ok) {
|
|
1594
|
+
const message =
|
|
1595
|
+
body.code === 'reserved_key'
|
|
1596
|
+
? t(
|
|
1597
|
+
'ai_assistant.agents.override.errors.reservedKey',
|
|
1598
|
+
'Prompt overrides cannot modify policy fields (mutationPolicy, readOnly, allowedTools, acceptedMediaTypes). Remove those sections and retry.',
|
|
1599
|
+
)
|
|
1600
|
+
: (body.error ?? `Failed to save overrides (${status}).`)
|
|
1601
|
+
throw new Error(message)
|
|
1602
|
+
}
|
|
1603
|
+
return body
|
|
924
1604
|
},
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
}
|
|
1605
|
+
context: {},
|
|
1606
|
+
})
|
|
938
1607
|
if (payload.ok === true && typeof payload.version === 'number') {
|
|
1608
|
+
const successMessage = t(
|
|
1609
|
+
'ai_assistant.agents.override.savedMessage',
|
|
1610
|
+
'Prompt override saved.',
|
|
1611
|
+
)
|
|
939
1612
|
setSaveState({
|
|
940
1613
|
kind: 'success',
|
|
941
1614
|
version: payload.version,
|
|
942
|
-
message:
|
|
943
|
-
'ai_assistant.agents.override.savedMessage',
|
|
944
|
-
'Prompt override saved.',
|
|
945
|
-
),
|
|
1615
|
+
message: successMessage,
|
|
946
1616
|
})
|
|
1617
|
+
flash(successMessage, 'success')
|
|
947
1618
|
await queryClient.invalidateQueries({
|
|
948
1619
|
queryKey: ['ai_assistant', 'agent_settings', 'override', agent.id],
|
|
949
1620
|
})
|
|
950
1621
|
return
|
|
951
1622
|
}
|
|
952
1623
|
// Legacy placeholder response: surfaces the Step-4.5 wording for BC.
|
|
1624
|
+
const legacyMessage =
|
|
1625
|
+
payload.message ??
|
|
1626
|
+
t(
|
|
1627
|
+
'ai_assistant.agents.prompt.pendingMessage',
|
|
1628
|
+
'Prompt overrides accepted.',
|
|
1629
|
+
)
|
|
953
1630
|
setSaveState({
|
|
954
1631
|
kind: 'success',
|
|
955
1632
|
version: 0,
|
|
956
|
-
message:
|
|
957
|
-
payload.message ??
|
|
958
|
-
t(
|
|
959
|
-
'ai_assistant.agents.prompt.pendingMessage',
|
|
960
|
-
'Prompt overrides accepted.',
|
|
961
|
-
),
|
|
1633
|
+
message: legacyMessage,
|
|
962
1634
|
})
|
|
1635
|
+
flash(legacyMessage, 'success')
|
|
963
1636
|
} catch (err) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
})
|
|
1637
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1638
|
+
setSaveState({ kind: 'error', message })
|
|
1639
|
+
flash(message, 'error')
|
|
968
1640
|
} finally {
|
|
969
1641
|
setIsSaving(false)
|
|
970
1642
|
}
|
|
971
|
-
}, [activeOverrides, agent.id, isSaving, queryClient, t])
|
|
1643
|
+
}, [activeOverrides, agent.id, isSaving, queryClient, runSavePromptOverrideMutation, t])
|
|
972
1644
|
|
|
973
1645
|
return (
|
|
974
1646
|
<div className="flex flex-col gap-4" data-ai-agent-detail={agent.id}>
|
|
975
1647
|
<section className="rounded-lg border border-border bg-background p-4">
|
|
976
1648
|
<h2 className="text-xl font-semibold">{agent.label}</h2>
|
|
977
1649
|
<p className="mt-1 text-sm text-muted-foreground">{agent.description}</p>
|
|
978
|
-
<dl className="mt-4 grid grid-cols-
|
|
979
|
-
<div>
|
|
1650
|
+
<dl className="mt-4 grid grid-cols-[repeat(auto-fit,minmax(min(100%,9rem),1fr))] gap-3 text-sm">
|
|
1651
|
+
<div className="min-w-0">
|
|
980
1652
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
981
1653
|
{t('ai_assistant.agents.meta.module', 'Module')}
|
|
982
1654
|
</dt>
|
|
983
|
-
<dd className="mt-1 font-mono text-xs">{agent.moduleId}</dd>
|
|
1655
|
+
<dd className="mt-1 break-all font-mono text-xs">{agent.moduleId}</dd>
|
|
984
1656
|
</div>
|
|
985
|
-
<div>
|
|
1657
|
+
<div className="min-w-0">
|
|
986
1658
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
987
1659
|
{t('ai_assistant.agents.meta.id', 'Agent id')}
|
|
988
1660
|
</dt>
|
|
989
|
-
<dd className="mt-1 font-mono text-xs">{agent.id}</dd>
|
|
1661
|
+
<dd className="mt-1 break-all font-mono text-xs">{agent.id}</dd>
|
|
990
1662
|
</div>
|
|
991
|
-
<div>
|
|
1663
|
+
<div className="min-w-0">
|
|
992
1664
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
993
1665
|
{t('ai_assistant.agents.meta.executionMode', 'Execution mode')}
|
|
994
1666
|
</dt>
|
|
@@ -998,7 +1670,7 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
998
1670
|
</StatusBadge>
|
|
999
1671
|
</dd>
|
|
1000
1672
|
</div>
|
|
1001
|
-
<div>
|
|
1673
|
+
<div className="min-w-0">
|
|
1002
1674
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1003
1675
|
{t('ai_assistant.agents.meta.mutationPolicy', 'Mutation policy')}
|
|
1004
1676
|
</dt>
|
|
@@ -1006,12 +1678,13 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1006
1678
|
<StatusBadge
|
|
1007
1679
|
variant={mutationPolicyStatusMap[agent.mutationPolicy] ?? 'neutral'}
|
|
1008
1680
|
dot
|
|
1681
|
+
className="max-w-full whitespace-normal break-all"
|
|
1009
1682
|
>
|
|
1010
1683
|
{agent.mutationPolicy}
|
|
1011
1684
|
</StatusBadge>
|
|
1012
1685
|
</dd>
|
|
1013
1686
|
</div>
|
|
1014
|
-
<div>
|
|
1687
|
+
<div className="min-w-0">
|
|
1015
1688
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1016
1689
|
{t('ai_assistant.agents.meta.readOnly', 'Read-only')}
|
|
1017
1690
|
</dt>
|
|
@@ -1021,7 +1694,7 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1021
1694
|
: t('ai_assistant.agents.meta.readOnlyNo', 'No')}
|
|
1022
1695
|
</dd>
|
|
1023
1696
|
</div>
|
|
1024
|
-
<div>
|
|
1697
|
+
<div className="min-w-0">
|
|
1025
1698
|
<dt className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1026
1699
|
{t('ai_assistant.agents.meta.maxSteps', 'Max steps')}
|
|
1027
1700
|
</dt>
|
|
@@ -1032,14 +1705,16 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1032
1705
|
</dl>
|
|
1033
1706
|
</section>
|
|
1034
1707
|
|
|
1708
|
+
<AgentModelOverrideSection agent={agent} />
|
|
1709
|
+
|
|
1035
1710
|
<MutationPolicySection agent={agent} />
|
|
1036
1711
|
|
|
1037
1712
|
<section
|
|
1038
1713
|
className="rounded-lg border border-border bg-background p-4"
|
|
1039
1714
|
data-ai-agent-prompt-editor={agent.id}
|
|
1040
1715
|
>
|
|
1041
|
-
<header className="flex items-
|
|
1042
|
-
<div>
|
|
1716
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
1717
|
+
<div className="min-w-0">
|
|
1043
1718
|
<h3 className="text-sm font-semibold">
|
|
1044
1719
|
{t('ai_assistant.agents.prompt.title', 'Prompt sections')}
|
|
1045
1720
|
</h3>
|
|
@@ -1152,8 +1827,8 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1152
1827
|
className="rounded-lg border border-border bg-background p-4"
|
|
1153
1828
|
data-ai-agent-tools-list={agent.id}
|
|
1154
1829
|
>
|
|
1155
|
-
<header className="flex items-
|
|
1156
|
-
<div>
|
|
1830
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
1831
|
+
<div className="min-w-0">
|
|
1157
1832
|
<h3 className="text-sm font-semibold">
|
|
1158
1833
|
{t('ai_assistant.agents.tools.title', 'Allowed tools')}
|
|
1159
1834
|
</h3>
|
|
@@ -1186,10 +1861,10 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1186
1861
|
className="rounded-lg border border-border bg-background p-4"
|
|
1187
1862
|
data-ai-agent-override-history={agent.id}
|
|
1188
1863
|
>
|
|
1189
|
-
<header className="flex items-
|
|
1190
|
-
<div className="flex items-
|
|
1864
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
1865
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
1191
1866
|
<History className="size-4 text-muted-foreground" aria-hidden />
|
|
1192
|
-
<div>
|
|
1867
|
+
<div className="min-w-0">
|
|
1193
1868
|
<h3 className="text-sm font-semibold">
|
|
1194
1869
|
{t('ai_assistant.agents.override.history.title', 'Prompt override history')}
|
|
1195
1870
|
</h3>
|
|
@@ -1244,7 +1919,7 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1244
1919
|
(overrideQuery.data?.versions ?? []).slice(0, 5).map((entry) => (
|
|
1245
1920
|
<div
|
|
1246
1921
|
key={entry.id}
|
|
1247
|
-
className="flex items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
|
|
1922
|
+
className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2"
|
|
1248
1923
|
data-ai-agent-override-history-row={entry.version}
|
|
1249
1924
|
>
|
|
1250
1925
|
<div className="flex flex-col min-w-0">
|
|
@@ -1269,8 +1944,8 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
1269
1944
|
className="rounded-lg border border-border bg-background p-4"
|
|
1270
1945
|
data-ai-agent-attachments={agent.id}
|
|
1271
1946
|
>
|
|
1272
|
-
<header className="flex items-
|
|
1273
|
-
<div>
|
|
1947
|
+
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
|
|
1948
|
+
<div className="min-w-0">
|
|
1274
1949
|
<h3 className="text-sm font-semibold">
|
|
1275
1950
|
{t('ai_assistant.agents.attachments.title', 'Attachment policy')}
|
|
1276
1951
|
</h3>
|
|
@@ -1357,8 +2032,8 @@ export function AiAgentSettingsPageClient() {
|
|
|
1357
2032
|
|
|
1358
2033
|
return (
|
|
1359
2034
|
<TooltipProvider delayDuration={200}>
|
|
1360
|
-
<div className="flex flex-col gap-4" data-ai-agent-settings>
|
|
1361
|
-
<header className="flex flex-col gap-1">
|
|
2035
|
+
<div className="flex min-w-0 flex-col gap-4" data-ai-agent-settings>
|
|
2036
|
+
<header className="flex min-w-0 flex-col gap-1">
|
|
1362
2037
|
<h1 className="text-2xl font-bold tracking-tight">
|
|
1363
2038
|
{t('ai_assistant.agents.title', 'AI Agents')}
|
|
1364
2039
|
</h1>
|
|
@@ -1374,24 +2049,30 @@ export function AiAgentSettingsPageClient() {
|
|
|
1374
2049
|
className="flex flex-col gap-3 rounded-lg border border-border bg-background p-3"
|
|
1375
2050
|
data-ai-agent-settings-picker-wrap
|
|
1376
2051
|
>
|
|
1377
|
-
<div className="flex flex-
|
|
1378
|
-
<div className="flex flex-col gap-2
|
|
2052
|
+
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
2053
|
+
<div className="flex min-w-[min(100%,16rem)] flex-1 flex-col gap-2">
|
|
1379
2054
|
<Label htmlFor="ai-agent-settings-picker">
|
|
1380
2055
|
{t('ai_assistant.agents.agentPickerLabel', 'Agent')}
|
|
1381
2056
|
</Label>
|
|
1382
|
-
<
|
|
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"
|
|
2057
|
+
<Select
|
|
1386
2058
|
value={selectedAgentId ?? ''}
|
|
1387
|
-
|
|
2059
|
+
onValueChange={(value) => setSelectedAgentId(value)}
|
|
1388
2060
|
>
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
2061
|
+
<SelectTrigger
|
|
2062
|
+
id="ai-agent-settings-picker"
|
|
2063
|
+
data-ai-agent-settings-picker
|
|
2064
|
+
className="w-full min-w-0"
|
|
2065
|
+
>
|
|
2066
|
+
<SelectValue />
|
|
2067
|
+
</SelectTrigger>
|
|
2068
|
+
<SelectContent>
|
|
2069
|
+
{agents.map((agent) => (
|
|
2070
|
+
<SelectItem key={agent.id} value={agent.id}>
|
|
2071
|
+
{agent.label} ({agent.id})
|
|
2072
|
+
</SelectItem>
|
|
2073
|
+
))}
|
|
2074
|
+
</SelectContent>
|
|
2075
|
+
</Select>
|
|
1395
2076
|
</div>
|
|
1396
2077
|
<div className="flex items-center gap-2 sm:flex-shrink-0">
|
|
1397
2078
|
<IconButton
|