@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
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { Bot, Loader2, CheckCircle2, XCircle, ChevronDown, ChevronRight, Server, Wrench, Eye, EyeOff, Database, Link2, Settings, Key, X } from 'lucide-react'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
|
+
import { AI_ASSISTANT_LAUNCHER_OPEN_EVENT } from '@open-mercato/ui/ai/AiAssistantLauncher'
|
|
9
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
11
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
|
|
13
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
9
14
|
import { useAiAssistantVisibility } from '../../../frontend/hooks/useAiAssistantVisibility'
|
|
10
15
|
import McpConfigDialog from './McpConfigDialog'
|
|
11
16
|
import SessionKeyDialog from './SessionKeyDialog'
|
|
12
17
|
|
|
13
|
-
// OpenCode health response type
|
|
14
18
|
type OpenCodeHealthResponse = {
|
|
15
19
|
status: 'ok' | 'error'
|
|
16
20
|
opencode?: {
|
|
@@ -28,23 +32,48 @@ type OpenCodeHealthResponse = {
|
|
|
28
32
|
message?: string
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
// Provider config type from settings API
|
|
32
35
|
type ProviderConfig = {
|
|
33
36
|
id: string
|
|
34
37
|
name: string
|
|
35
38
|
model: string
|
|
36
39
|
defaultModel: string
|
|
37
|
-
envKey: string
|
|
40
|
+
envKey: string | null
|
|
38
41
|
configured: boolean
|
|
42
|
+
defaultModels: Array<{ id: string; name: string }>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type TenantOverride = {
|
|
46
|
+
providerId: string | null
|
|
47
|
+
modelId: string | null
|
|
48
|
+
baseURL: string | null
|
|
49
|
+
agentId: string | null
|
|
50
|
+
updatedAt: string
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
type SettingsResponse = {
|
|
42
54
|
provider: ProviderConfig
|
|
43
55
|
availableProviders: ProviderConfig[]
|
|
44
56
|
mcpKeyConfigured: boolean
|
|
57
|
+
resolvedDefault: {
|
|
58
|
+
providerId: string
|
|
59
|
+
modelId: string
|
|
60
|
+
baseURL: string | null
|
|
61
|
+
source: string
|
|
62
|
+
} | null
|
|
63
|
+
tenantOverride: TenantOverride | null
|
|
64
|
+
agents: AgentResolution[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type AgentResolution = {
|
|
68
|
+
agentId: string
|
|
69
|
+
moduleId: string
|
|
70
|
+
allowRuntimeModelOverride: boolean
|
|
71
|
+
providerId: string
|
|
72
|
+
modelId: string
|
|
73
|
+
baseURL: string | null
|
|
74
|
+
source: string
|
|
45
75
|
}
|
|
46
76
|
|
|
47
|
-
// Tool info type
|
|
48
77
|
type ToolInfo = {
|
|
49
78
|
name: string
|
|
50
79
|
description: string
|
|
@@ -52,37 +81,401 @@ type ToolInfo = {
|
|
|
52
81
|
inputSchema: Record<string, unknown>
|
|
53
82
|
}
|
|
54
83
|
|
|
55
|
-
|
|
84
|
+
type AiAssistantSettingsPageClientProps = {
|
|
85
|
+
launchMode?: 'selector' | 'legacy'
|
|
86
|
+
showVisibilityControl?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const LEGACY_AI_ASSISTANT_OPEN_EVENT = 'om:open-ai-chat'
|
|
90
|
+
|
|
56
91
|
async function fetchHealth(): Promise<OpenCodeHealthResponse> {
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
-
return
|
|
92
|
+
const result = await apiCall<OpenCodeHealthResponse>('/api/ai_assistant/health')
|
|
93
|
+
if (!result.ok || !result.result) throw new Error('Failed to fetch health')
|
|
94
|
+
return result.result
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
async function fetchSettings(): Promise<SettingsResponse> {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
return
|
|
98
|
+
const result = await apiCall<SettingsResponse>('/api/ai_assistant/settings')
|
|
99
|
+
if (!result.ok || !result.result) throw new Error('Failed to fetch settings')
|
|
100
|
+
return result.result
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
async function fetchTools(): Promise<{ tools: ToolInfo[] }> {
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
return
|
|
104
|
+
const result = await apiCall<{ tools: ToolInfo[] }>('/api/ai_assistant/tools')
|
|
105
|
+
if (!result.ok || !result.result) throw new Error('Failed to fetch tools')
|
|
106
|
+
return result.result
|
|
72
107
|
}
|
|
73
108
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
function GlobalOverrideForm({
|
|
110
|
+
availableProviders,
|
|
111
|
+
tenantOverride,
|
|
112
|
+
onSaved,
|
|
113
|
+
}: {
|
|
114
|
+
availableProviders: ProviderConfig[]
|
|
115
|
+
tenantOverride: TenantOverride | null
|
|
116
|
+
onSaved: () => void
|
|
117
|
+
}) {
|
|
118
|
+
const t = useT()
|
|
119
|
+
const [selectedProviderId, setSelectedProviderId] = React.useState<string>(
|
|
120
|
+
tenantOverride?.providerId ?? '',
|
|
121
|
+
)
|
|
122
|
+
const [selectedModelId, setSelectedModelId] = React.useState<string>(
|
|
123
|
+
tenantOverride?.modelId ?? '',
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const selectedProvider = availableProviders.find((p) => p.id === selectedProviderId)
|
|
127
|
+
|
|
128
|
+
const { runMutation: runSaveMutation } = useGuardedMutation({ contextId: 'ai-settings-save-override' })
|
|
129
|
+
const { runMutation: runClearMutation } = useGuardedMutation({ contextId: 'ai-settings-clear-override' })
|
|
130
|
+
const [isSaving, setIsSaving] = React.useState(false)
|
|
131
|
+
const [isClearing, setIsClearing] = React.useState(false)
|
|
132
|
+
|
|
133
|
+
const handleSave = React.useCallback(async () => {
|
|
134
|
+
setIsSaving(true)
|
|
135
|
+
try {
|
|
136
|
+
await runSaveMutation({
|
|
137
|
+
operation: async () => {
|
|
138
|
+
const result = await apiCall('/api/ai_assistant/settings', {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
providerId: selectedProviderId || null,
|
|
143
|
+
modelId: selectedModelId || null,
|
|
144
|
+
}),
|
|
145
|
+
})
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
const err = (result.result as { error?: string } | null)?.error
|
|
148
|
+
throw new Error(err ?? t('ai_assistant.settings.saveError', 'Failed to save override.'))
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
context: {},
|
|
152
|
+
})
|
|
153
|
+
flash(t('ai_assistant.settings.saveSuccess', 'Default model override saved.'), 'success')
|
|
154
|
+
onSaved()
|
|
155
|
+
} finally {
|
|
156
|
+
setIsSaving(false)
|
|
157
|
+
}
|
|
158
|
+
}, [onSaved, runSaveMutation, selectedModelId, selectedProviderId, t])
|
|
159
|
+
|
|
160
|
+
const handleClear = React.useCallback(async () => {
|
|
161
|
+
setIsClearing(true)
|
|
162
|
+
try {
|
|
163
|
+
await runClearMutation({
|
|
164
|
+
operation: async () => {
|
|
165
|
+
const result = await apiCall('/api/ai_assistant/settings', {
|
|
166
|
+
method: 'DELETE',
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({}),
|
|
169
|
+
})
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
const err = (result.result as { error?: string } | null)?.error
|
|
172
|
+
throw new Error(err ?? t('ai_assistant.settings.clearError', 'Failed to clear override.'))
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
context: {},
|
|
176
|
+
})
|
|
177
|
+
flash(t('ai_assistant.settings.clearSuccess', 'Default model override cleared.'), 'success')
|
|
178
|
+
setSelectedProviderId('')
|
|
179
|
+
setSelectedModelId('')
|
|
180
|
+
onSaved()
|
|
181
|
+
} finally {
|
|
182
|
+
setIsClearing(false)
|
|
183
|
+
}
|
|
184
|
+
}, [runClearMutation, onSaved, t])
|
|
185
|
+
|
|
186
|
+
const isBusy = isSaving || isClearing
|
|
187
|
+
|
|
188
|
+
const configuredProviders = availableProviders.filter((p) => p.configured)
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className="rounded-lg border bg-card p-6" data-ai-settings-override-form="">
|
|
192
|
+
<h2 className="mb-1 text-sm font-semibold">
|
|
193
|
+
{t('ai_assistant.settings.defaultOverrideTitle', 'Default model override')}
|
|
194
|
+
</h2>
|
|
195
|
+
<p className="mb-4 text-xs text-muted-foreground">
|
|
196
|
+
{t(
|
|
197
|
+
'ai_assistant.settings.defaultOverrideDescription',
|
|
198
|
+
'Set a tenant-wide default provider and model. Agents with a per-agent override or specific defaultModel will take precedence.',
|
|
199
|
+
)}
|
|
200
|
+
</p>
|
|
201
|
+
{tenantOverride && (tenantOverride.providerId || tenantOverride.modelId) ? (
|
|
202
|
+
<div className="mb-4 flex items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2 text-xs">
|
|
203
|
+
<span className="text-muted-foreground">
|
|
204
|
+
{t('ai_assistant.settings.currentOverride', 'Current override:')}
|
|
205
|
+
</span>
|
|
206
|
+
<span className="font-medium">
|
|
207
|
+
{tenantOverride.providerId ?? '—'} / {tenantOverride.modelId ?? '—'}
|
|
208
|
+
</span>
|
|
209
|
+
<Button
|
|
210
|
+
type="button"
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="sm"
|
|
213
|
+
className="ml-auto h-6 gap-1 text-xs"
|
|
214
|
+
disabled={isBusy}
|
|
215
|
+
onClick={handleClear}
|
|
216
|
+
data-ai-settings-clear-override=""
|
|
217
|
+
>
|
|
218
|
+
<X className="size-3" aria-hidden />
|
|
219
|
+
{t('ai_assistant.settings.clearOverride', 'Clear override')}
|
|
220
|
+
</Button>
|
|
221
|
+
</div>
|
|
222
|
+
) : null}
|
|
223
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
224
|
+
<div className="flex flex-col gap-1">
|
|
225
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
226
|
+
{t('ai_assistant.settings.providerLabel', 'Provider')}
|
|
227
|
+
</span>
|
|
228
|
+
<Select
|
|
229
|
+
value={selectedProviderId}
|
|
230
|
+
onValueChange={(val) => {
|
|
231
|
+
setSelectedProviderId(val)
|
|
232
|
+
setSelectedModelId('')
|
|
233
|
+
}}
|
|
234
|
+
disabled={isBusy}
|
|
235
|
+
>
|
|
236
|
+
<SelectTrigger
|
|
237
|
+
className="w-[180px]"
|
|
238
|
+
data-ai-settings-provider-select=""
|
|
239
|
+
>
|
|
240
|
+
<SelectValue placeholder={t('ai_assistant.settings.selectProvider', 'Select provider')} />
|
|
241
|
+
</SelectTrigger>
|
|
242
|
+
<SelectContent>
|
|
243
|
+
{configuredProviders.map((provider) => (
|
|
244
|
+
<SelectItem key={provider.id} value={provider.id}>
|
|
245
|
+
{provider.name}
|
|
246
|
+
</SelectItem>
|
|
247
|
+
))}
|
|
248
|
+
</SelectContent>
|
|
249
|
+
</Select>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="flex flex-col gap-1">
|
|
252
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
253
|
+
{t('ai_assistant.settings.modelLabel', 'Model')}
|
|
254
|
+
</span>
|
|
255
|
+
<Select
|
|
256
|
+
value={selectedModelId}
|
|
257
|
+
onValueChange={setSelectedModelId}
|
|
258
|
+
disabled={isBusy || !selectedProviderId}
|
|
259
|
+
>
|
|
260
|
+
<SelectTrigger
|
|
261
|
+
className="w-[220px]"
|
|
262
|
+
data-ai-settings-model-select=""
|
|
263
|
+
>
|
|
264
|
+
<SelectValue placeholder={t('ai_assistant.settings.selectModel', 'Select model')} />
|
|
265
|
+
</SelectTrigger>
|
|
266
|
+
<SelectContent>
|
|
267
|
+
{(selectedProvider?.defaultModels ?? []).map((model) => (
|
|
268
|
+
<SelectItem key={model.id} value={model.id}>
|
|
269
|
+
{model.name}
|
|
270
|
+
</SelectItem>
|
|
271
|
+
))}
|
|
272
|
+
</SelectContent>
|
|
273
|
+
</Select>
|
|
274
|
+
</div>
|
|
275
|
+
<Button
|
|
276
|
+
type="button"
|
|
277
|
+
size="sm"
|
|
278
|
+
disabled={isBusy || !selectedProviderId || !selectedModelId}
|
|
279
|
+
onClick={handleSave}
|
|
280
|
+
data-ai-settings-save-override=""
|
|
281
|
+
>
|
|
282
|
+
{isSaving ? (
|
|
283
|
+
<Loader2 className="size-3.5 animate-spin" aria-hidden />
|
|
284
|
+
) : null}
|
|
285
|
+
{t('ai_assistant.settings.saveOverride', 'Save override')}
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function PerAgentOverrideList({
|
|
293
|
+
agents,
|
|
294
|
+
onCleared,
|
|
295
|
+
}: {
|
|
296
|
+
agents: AgentResolution[]
|
|
297
|
+
onCleared: () => void
|
|
298
|
+
}) {
|
|
299
|
+
const t = useT()
|
|
300
|
+
const { runMutation: runClearAgentMutation } = useGuardedMutation({ contextId: 'ai-settings-clear-agent-override' })
|
|
301
|
+
const [clearingAgentId, setClearingAgentId] = React.useState<string | null>(null)
|
|
302
|
+
|
|
303
|
+
const handleClearAgentOverride = React.useCallback(
|
|
304
|
+
async (agentId: string) => {
|
|
305
|
+
setClearingAgentId(agentId)
|
|
306
|
+
try {
|
|
307
|
+
await runClearAgentMutation({
|
|
308
|
+
operation: async () => {
|
|
309
|
+
const result = await apiCall('/api/ai_assistant/settings', {
|
|
310
|
+
method: 'DELETE',
|
|
311
|
+
headers: { 'Content-Type': 'application/json' },
|
|
312
|
+
body: JSON.stringify({ agentId }),
|
|
313
|
+
})
|
|
314
|
+
if (!result.ok) {
|
|
315
|
+
const err = (result.result as { error?: string } | null)?.error
|
|
316
|
+
throw new Error(err ?? t('ai_assistant.settings.clearAgentError', 'Failed to clear agent override.'))
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
context: {},
|
|
320
|
+
})
|
|
321
|
+
flash(t('ai_assistant.settings.clearAgentSuccess', 'Agent override cleared.'), 'success')
|
|
322
|
+
onCleared()
|
|
323
|
+
} finally {
|
|
324
|
+
setClearingAgentId(null)
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
[runClearAgentMutation, onCleared, t],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if (agents.length === 0) return null
|
|
331
|
+
|
|
332
|
+
const overriddenAgents = agents.filter((agent) => agent.source !== 'env_default' && agent.source !== 'provider_default')
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div className="rounded-lg border bg-card p-6" data-ai-settings-agent-overrides="">
|
|
336
|
+
<h2 className="mb-1 text-sm font-semibold">
|
|
337
|
+
{t('ai_assistant.settings.agentOverridesTitle', 'Per-agent model resolution')}
|
|
338
|
+
</h2>
|
|
339
|
+
<p className="mb-4 text-xs text-muted-foreground">
|
|
340
|
+
{t(
|
|
341
|
+
'ai_assistant.settings.agentOverridesDescription',
|
|
342
|
+
'Resolved model for each registered agent. Agents with a custom override show a Clear button.',
|
|
343
|
+
)}
|
|
344
|
+
</p>
|
|
345
|
+
<div className="overflow-x-auto">
|
|
346
|
+
<table className="w-full text-xs">
|
|
347
|
+
<thead>
|
|
348
|
+
<tr className="border-b border-border text-left text-muted-foreground">
|
|
349
|
+
<th className="pb-2 pr-4 font-medium">
|
|
350
|
+
{t('ai_assistant.settings.agentIdColumn', 'Agent')}
|
|
351
|
+
</th>
|
|
352
|
+
<th className="pb-2 pr-4 font-medium">
|
|
353
|
+
{t('ai_assistant.settings.providerColumn', 'Provider')}
|
|
354
|
+
</th>
|
|
355
|
+
<th className="pb-2 pr-4 font-medium">
|
|
356
|
+
{t('ai_assistant.settings.modelColumn', 'Model')}
|
|
357
|
+
</th>
|
|
358
|
+
<th className="pb-2 pr-4 font-medium">
|
|
359
|
+
{t('ai_assistant.settings.sourceColumn', 'Source')}
|
|
360
|
+
</th>
|
|
361
|
+
<th className="pb-2 font-medium" />
|
|
362
|
+
</tr>
|
|
363
|
+
</thead>
|
|
364
|
+
<tbody>
|
|
365
|
+
{agents.map((agent) => {
|
|
366
|
+
const hasOverride = overriddenAgents.some((a) => a.agentId === agent.agentId)
|
|
367
|
+
return (
|
|
368
|
+
<tr
|
|
369
|
+
key={agent.agentId}
|
|
370
|
+
className="border-b border-border/50 last:border-0"
|
|
371
|
+
data-ai-settings-agent-row={agent.agentId}
|
|
372
|
+
>
|
|
373
|
+
<td className="py-2 pr-4 font-mono">{agent.agentId}</td>
|
|
374
|
+
<td className="py-2 pr-4">{agent.providerId}</td>
|
|
375
|
+
<td className="py-2 pr-4">{agent.modelId}</td>
|
|
376
|
+
<td className="py-2 pr-4 text-muted-foreground">{agent.source}</td>
|
|
377
|
+
<td className="py-2">
|
|
378
|
+
{hasOverride ? (
|
|
379
|
+
<Button
|
|
380
|
+
type="button"
|
|
381
|
+
variant="ghost"
|
|
382
|
+
size="sm"
|
|
383
|
+
className="h-6 gap-1 text-xs"
|
|
384
|
+
disabled={clearingAgentId !== null}
|
|
385
|
+
onClick={() => void handleClearAgentOverride(agent.agentId)}
|
|
386
|
+
data-ai-settings-clear-agent-override={agent.agentId}
|
|
387
|
+
>
|
|
388
|
+
<X className="size-3" aria-hidden />
|
|
389
|
+
{t('ai_assistant.settings.clearOverride', 'Clear override')}
|
|
390
|
+
</Button>
|
|
391
|
+
) : null}
|
|
392
|
+
</td>
|
|
393
|
+
</tr>
|
|
394
|
+
)
|
|
395
|
+
})}
|
|
396
|
+
</tbody>
|
|
397
|
+
</table>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function AiAssistantLauncherCard({
|
|
404
|
+
launchMode,
|
|
405
|
+
showVisibilityControl,
|
|
406
|
+
}: {
|
|
407
|
+
launchMode: 'selector' | 'legacy'
|
|
408
|
+
showVisibilityControl: boolean
|
|
409
|
+
}) {
|
|
410
|
+
const t = useT()
|
|
78
411
|
const { isEnabled, toggleEnabled, isLoaded } = useAiAssistantVisibility()
|
|
79
412
|
|
|
80
|
-
// Open AI Assistant by dispatching global event (triggers main layout's DockableChat)
|
|
81
413
|
const openAiAssistant = () => {
|
|
82
|
-
|
|
414
|
+
const eventName = launchMode === 'legacy'
|
|
415
|
+
? LEGACY_AI_ASSISTANT_OPEN_EVENT
|
|
416
|
+
: AI_ASSISTANT_LAUNCHER_OPEN_EVENT
|
|
417
|
+
window.dispatchEvent(new CustomEvent(eventName))
|
|
83
418
|
}
|
|
84
419
|
|
|
85
|
-
|
|
420
|
+
return (
|
|
421
|
+
<div className="rounded-lg border bg-card p-6">
|
|
422
|
+
<div className="flex items-start justify-between gap-4">
|
|
423
|
+
<div className="space-y-1">
|
|
424
|
+
<h2 className="flex items-center gap-2 text-lg font-semibold">
|
|
425
|
+
<Bot className="size-5" />
|
|
426
|
+
{t('ai_assistant.settings.visibilityTitle', 'AI Assistant')}
|
|
427
|
+
</h2>
|
|
428
|
+
<p className="text-sm text-muted-foreground">
|
|
429
|
+
{showVisibilityControl
|
|
430
|
+
? isEnabled
|
|
431
|
+
? t('ai_assistant.settings.visibilityEnabled', 'Visible in header with Cmd+J shortcut enabled.')
|
|
432
|
+
: t('ai_assistant.settings.visibilityDisabled', 'Hidden from header. Enable to show the button and Cmd+J shortcut.')
|
|
433
|
+
: t('ai_assistant.settings.launchDescription', 'Open the AI assistant from this page.')}
|
|
434
|
+
</p>
|
|
435
|
+
</div>
|
|
436
|
+
<div className="flex items-center gap-4">
|
|
437
|
+
{showVisibilityControl ? (
|
|
438
|
+
<div className="flex items-center gap-3 rounded-md border bg-muted/30 px-4 py-2">
|
|
439
|
+
<span className="text-sm font-medium">
|
|
440
|
+
{t('ai_assistant.settings.visibilityToggleLabel', 'Visibility')}
|
|
441
|
+
</span>
|
|
442
|
+
{isEnabled ? (
|
|
443
|
+
<Eye className="size-4 text-muted-foreground" />
|
|
444
|
+
) : (
|
|
445
|
+
<EyeOff className="size-4 text-muted-foreground" />
|
|
446
|
+
)}
|
|
447
|
+
<Switch
|
|
448
|
+
checked={isEnabled}
|
|
449
|
+
onCheckedChange={toggleEnabled}
|
|
450
|
+
disabled={!isLoaded}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
) : null}
|
|
454
|
+
<Button type="button" onClick={openAiAssistant} size="default" className="gap-2">
|
|
455
|
+
<Bot className="size-4" />
|
|
456
|
+
{launchMode === 'legacy'
|
|
457
|
+
? t('ai_assistant.settings.openButton', 'Open AI Assistant')
|
|
458
|
+
: t('ai_assistant.settings.openSelectorButton', 'Open AI assistants')}
|
|
459
|
+
</Button>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function AiAssistantSettingsContent({
|
|
467
|
+
launchMode,
|
|
468
|
+
showVisibilityControl,
|
|
469
|
+
}: {
|
|
470
|
+
launchMode: 'selector' | 'legacy'
|
|
471
|
+
showVisibilityControl: boolean
|
|
472
|
+
}) {
|
|
473
|
+
const t = useT()
|
|
474
|
+
const queryClient = useQueryClient()
|
|
475
|
+
const [toolsExpanded, setToolsExpanded] = React.useState(false)
|
|
476
|
+
const [mcpConfigOpen, setMcpConfigOpen] = React.useState(false)
|
|
477
|
+
const [sessionKeyOpen, setSessionKeyOpen] = React.useState(false)
|
|
478
|
+
|
|
86
479
|
const healthQuery = useQuery({
|
|
87
480
|
queryKey: ['ai-assistant', 'health'],
|
|
88
481
|
queryFn: fetchHealth,
|
|
@@ -90,36 +483,37 @@ function AiAssistantSettingsContent() {
|
|
|
90
483
|
staleTime: 5000,
|
|
91
484
|
})
|
|
92
485
|
|
|
93
|
-
// Settings query - no polling needed (static config)
|
|
94
486
|
const settingsQuery = useQuery({
|
|
95
487
|
queryKey: ['ai-assistant', 'settings'],
|
|
96
488
|
queryFn: fetchSettings,
|
|
97
489
|
staleTime: 60000,
|
|
98
490
|
})
|
|
99
491
|
|
|
100
|
-
// Tools query - no polling needed
|
|
101
492
|
const toolsQuery = useQuery({
|
|
102
493
|
queryKey: ['ai-assistant', 'tools'],
|
|
103
494
|
queryFn: fetchTools,
|
|
104
495
|
staleTime: 60000,
|
|
105
496
|
})
|
|
106
497
|
|
|
498
|
+
const handleOverrideSaved = React.useCallback(() => {
|
|
499
|
+
void queryClient.invalidateQueries({ queryKey: ['ai-assistant', 'settings'] })
|
|
500
|
+
}, [queryClient])
|
|
501
|
+
|
|
107
502
|
const isLoading = healthQuery.isLoading || settingsQuery.isLoading || toolsQuery.isLoading
|
|
108
503
|
|
|
109
504
|
if (isLoading) {
|
|
110
505
|
return (
|
|
111
|
-
<div className="flex items-center gap-2 text-muted-foreground
|
|
112
|
-
<Loader2 className="
|
|
113
|
-
Loading settings...
|
|
506
|
+
<div className="flex items-center gap-2 py-8 text-muted-foreground">
|
|
507
|
+
<Loader2 className="size-4 animate-spin" />
|
|
508
|
+
{t('ai_assistant.settings.loading', 'Loading settings...')}
|
|
114
509
|
</div>
|
|
115
510
|
)
|
|
116
511
|
}
|
|
117
512
|
|
|
118
513
|
const health = healthQuery.data
|
|
119
514
|
const settings = settingsQuery.data
|
|
120
|
-
const tools = toolsQuery.data?.tools
|
|
515
|
+
const tools = toolsQuery.data?.tools ?? []
|
|
121
516
|
|
|
122
|
-
// Group tools by module
|
|
123
517
|
const toolsByModule = tools.reduce<Record<string, ToolInfo[]>>((acc, tool) => {
|
|
124
518
|
const module = tool.module || 'other'
|
|
125
519
|
if (!acc[module]) acc[module] = []
|
|
@@ -130,264 +524,266 @@ function AiAssistantSettingsContent() {
|
|
|
130
524
|
const provider = settings?.provider
|
|
131
525
|
|
|
132
526
|
return (
|
|
133
|
-
<div className="flex flex-col gap-6">
|
|
134
|
-
{/* Header */}
|
|
527
|
+
<div className="flex flex-col gap-6" data-ai-assistant-settings="">
|
|
135
528
|
<div className="space-y-1">
|
|
136
|
-
<h1 className="
|
|
137
|
-
<Bot className="
|
|
138
|
-
AI Assistant Settings
|
|
529
|
+
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
|
530
|
+
<Bot className="size-6" />
|
|
531
|
+
{t('ai_assistant.settings.pageTitle', 'AI Assistant Settings')}
|
|
139
532
|
</h1>
|
|
140
533
|
<p className="text-muted-foreground">
|
|
141
|
-
Configure and monitor the AI assistant
|
|
534
|
+
{t('ai_assistant.settings.pageDescription', 'Configure and monitor the AI assistant')}
|
|
142
535
|
</p>
|
|
143
536
|
</div>
|
|
144
537
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
checked={isEnabled}
|
|
165
|
-
onCheckedChange={toggleEnabled}
|
|
166
|
-
disabled={!isLoaded}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
<Button onClick={openAiAssistant} size="default" className="gap-2">
|
|
170
|
-
<Bot className="h-4 w-4" />
|
|
171
|
-
Open AI Assistant
|
|
172
|
-
</Button>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
538
|
+
<AiAssistantLauncherCard
|
|
539
|
+
launchMode={launchMode}
|
|
540
|
+
showVisibilityControl={showVisibilityControl}
|
|
541
|
+
/>
|
|
542
|
+
|
|
543
|
+
{settings ? (
|
|
544
|
+
<GlobalOverrideForm
|
|
545
|
+
availableProviders={settings.availableProviders}
|
|
546
|
+
tenantOverride={settings.tenantOverride}
|
|
547
|
+
onSaved={handleOverrideSaved}
|
|
548
|
+
/>
|
|
549
|
+
) : null}
|
|
550
|
+
|
|
551
|
+
{settings?.agents && settings.agents.length > 0 ? (
|
|
552
|
+
<PerAgentOverrideList
|
|
553
|
+
agents={settings.agents}
|
|
554
|
+
onCleared={handleOverrideSaved}
|
|
555
|
+
/>
|
|
556
|
+
) : null}
|
|
176
557
|
|
|
177
|
-
{/* Connections Section */}
|
|
178
558
|
<div className="rounded-lg border bg-card p-6">
|
|
179
|
-
<div className="flex items-center justify-between
|
|
180
|
-
<h2 className="
|
|
181
|
-
<Link2 className="
|
|
182
|
-
Connections
|
|
559
|
+
<div className="mb-4 flex items-center justify-between">
|
|
560
|
+
<h2 className="flex items-center gap-2 text-sm font-semibold">
|
|
561
|
+
<Link2 className="size-4" />
|
|
562
|
+
{t('ai_assistant.settings.connectionsTitle', 'Connections')}
|
|
183
563
|
</h2>
|
|
184
|
-
{healthQuery.isFetching && !healthQuery.isLoading
|
|
185
|
-
<Loader2 className="
|
|
186
|
-
)}
|
|
564
|
+
{healthQuery.isFetching && !healthQuery.isLoading ? (
|
|
565
|
+
<Loader2 className="size-3 animate-spin text-muted-foreground" />
|
|
566
|
+
) : null}
|
|
187
567
|
</div>
|
|
188
568
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
569
|
+
<div
|
|
570
|
+
className={`rounded-lg border-2 p-4 ${
|
|
571
|
+
health?.status === 'ok' && health.opencode?.healthy
|
|
572
|
+
? 'border-status-success-border bg-status-success-bg'
|
|
573
|
+
: 'border-destructive/50 bg-destructive/5'
|
|
574
|
+
}`}
|
|
575
|
+
>
|
|
195
576
|
<div className="flex items-start justify-between">
|
|
196
|
-
<div className="
|
|
577
|
+
<div className="min-w-0 flex-1">
|
|
197
578
|
<div className="flex items-center gap-2">
|
|
198
|
-
<Server className="
|
|
579
|
+
<Server className="size-4 shrink-0 text-muted-foreground" />
|
|
199
580
|
<p className="text-sm font-medium">OpenCode</p>
|
|
200
581
|
</div>
|
|
201
|
-
<p className="text-xs text-muted-foreground
|
|
582
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
202
583
|
{health?.status === 'ok' && health.opencode?.healthy ? (
|
|
203
584
|
<span className="flex items-center gap-1 text-status-success-text">
|
|
204
|
-
<CheckCircle2 className="
|
|
205
|
-
Connected
|
|
585
|
+
<CheckCircle2 className="size-3" />
|
|
586
|
+
{t('ai_assistant.settings.connected', 'Connected')}
|
|
206
587
|
</span>
|
|
207
588
|
) : (
|
|
208
589
|
<span className="flex items-center gap-1 text-destructive">
|
|
209
|
-
<XCircle className="
|
|
210
|
-
{health?.message
|
|
590
|
+
<XCircle className="size-3" />
|
|
591
|
+
{health?.message ?? t('ai_assistant.settings.disconnected', 'Disconnected')}
|
|
211
592
|
</span>
|
|
212
593
|
)}
|
|
213
594
|
</p>
|
|
214
|
-
{health?.opencode?.version
|
|
215
|
-
<p className="text-xs text-muted-foreground
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<p className="text-xs text-muted-foreground mt-1 truncate">
|
|
220
|
-
{health?.url || 'Not configured'}
|
|
595
|
+
{health?.opencode?.version ? (
|
|
596
|
+
<p className="mt-1 text-xs text-muted-foreground">v{health.opencode.version}</p>
|
|
597
|
+
) : null}
|
|
598
|
+
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
599
|
+
{health?.url ?? t('ai_assistant.settings.notConfigured', 'Not configured')}
|
|
221
600
|
</p>
|
|
222
601
|
</div>
|
|
223
|
-
{health?.status === 'ok' && health.opencode?.healthy
|
|
224
|
-
<div className="flex
|
|
225
|
-
<svg className="
|
|
602
|
+
{health?.status === 'ok' && health.opencode?.healthy ? (
|
|
603
|
+
<div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-status-success-bg text-status-success-icon">
|
|
604
|
+
<svg className="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
226
605
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
227
606
|
</svg>
|
|
228
607
|
</div>
|
|
229
|
-
)}
|
|
608
|
+
) : null}
|
|
230
609
|
</div>
|
|
231
610
|
</div>
|
|
232
611
|
|
|
233
|
-
{/* MCP Server Status */}
|
|
234
612
|
{(() => {
|
|
235
|
-
const mcpConnected =
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
613
|
+
const mcpConnected =
|
|
614
|
+
health?.mcp && Object.values(health.mcp).some((s) => s.status === 'connected')
|
|
615
|
+
const mcpConnecting =
|
|
616
|
+
health?.mcp && Object.values(health.mcp).some((s) => s.status === 'connecting')
|
|
617
|
+
const mcpError =
|
|
618
|
+
health?.mcp && Object.values(health.mcp).find((s) => s.error)?.error
|
|
239
619
|
return (
|
|
240
|
-
<div
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
620
|
+
<div
|
|
621
|
+
className={`rounded-lg border-2 p-4 ${
|
|
622
|
+
mcpConnected
|
|
623
|
+
? 'border-status-success-border bg-status-success-bg'
|
|
624
|
+
: mcpConnecting
|
|
625
|
+
? 'border-status-warning-border bg-status-warning-bg'
|
|
626
|
+
: 'border-destructive/50 bg-destructive/5'
|
|
627
|
+
}`}
|
|
628
|
+
>
|
|
247
629
|
<div className="flex items-start justify-between">
|
|
248
|
-
<div className="
|
|
630
|
+
<div className="min-w-0 flex-1">
|
|
249
631
|
<div className="flex items-center gap-2">
|
|
250
|
-
<Wrench className="
|
|
632
|
+
<Wrench className="size-4 shrink-0 text-muted-foreground" />
|
|
251
633
|
<p className="text-sm font-medium">MCP Server</p>
|
|
252
634
|
</div>
|
|
253
|
-
<p className="text-xs text-muted-foreground
|
|
635
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
254
636
|
{mcpConnected ? (
|
|
255
637
|
<span className="flex items-center gap-1 text-status-success-text">
|
|
256
|
-
<CheckCircle2 className="
|
|
257
|
-
Connected
|
|
638
|
+
<CheckCircle2 className="size-3" />
|
|
639
|
+
{t('ai_assistant.settings.connected', 'Connected')}
|
|
258
640
|
</span>
|
|
259
641
|
) : mcpConnecting ? (
|
|
260
642
|
<span className="flex items-center gap-1 text-status-warning-text">
|
|
261
|
-
<Loader2 className="
|
|
262
|
-
Connecting...
|
|
643
|
+
<Loader2 className="size-3 animate-spin" />
|
|
644
|
+
{t('ai_assistant.settings.connecting', 'Connecting...')}
|
|
263
645
|
</span>
|
|
264
646
|
) : (
|
|
265
647
|
<span className="flex items-center gap-1 text-destructive">
|
|
266
|
-
<XCircle className="
|
|
267
|
-
{mcpError
|
|
648
|
+
<XCircle className="size-3" />
|
|
649
|
+
{mcpError ?? t('ai_assistant.settings.disconnected', 'Disconnected')}
|
|
268
650
|
</span>
|
|
269
651
|
)}
|
|
270
652
|
</p>
|
|
271
|
-
<p className="text-xs text-muted-foreground
|
|
272
|
-
{health?.mcpUrl
|
|
653
|
+
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
654
|
+
{health?.mcpUrl ?? t('ai_assistant.settings.notConfigured', 'Not configured')}
|
|
273
655
|
</p>
|
|
274
656
|
</div>
|
|
275
|
-
{mcpConnected
|
|
276
|
-
<div className="flex
|
|
277
|
-
<svg className="
|
|
657
|
+
{mcpConnected ? (
|
|
658
|
+
<div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-status-success-bg text-status-success-icon">
|
|
659
|
+
<svg className="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
278
660
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
279
661
|
</svg>
|
|
280
662
|
</div>
|
|
281
|
-
)}
|
|
663
|
+
) : null}
|
|
282
664
|
</div>
|
|
283
665
|
</div>
|
|
284
666
|
)
|
|
285
667
|
})()}
|
|
286
668
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
669
|
+
<div
|
|
670
|
+
className={`rounded-lg border-2 p-4 ${
|
|
671
|
+
health?.search?.available
|
|
672
|
+
? 'border-status-success-border bg-status-success-bg'
|
|
673
|
+
: 'border-destructive/50 bg-destructive/5'
|
|
674
|
+
}`}
|
|
675
|
+
>
|
|
293
676
|
<div className="flex items-start justify-between">
|
|
294
|
-
<div className="
|
|
677
|
+
<div className="min-w-0 flex-1">
|
|
295
678
|
<div className="flex items-center gap-2">
|
|
296
|
-
<Database className="
|
|
679
|
+
<Database className="size-4 shrink-0 text-muted-foreground" />
|
|
297
680
|
<p className="text-sm font-medium">Meilisearch</p>
|
|
298
681
|
</div>
|
|
299
|
-
<p className="text-xs text-muted-foreground
|
|
682
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
300
683
|
{health?.search?.available ? (
|
|
301
684
|
<span className="flex items-center gap-1 text-status-success-text">
|
|
302
|
-
<CheckCircle2 className="
|
|
303
|
-
Connected
|
|
685
|
+
<CheckCircle2 className="size-3" />
|
|
686
|
+
{t('ai_assistant.settings.connected', 'Connected')}
|
|
304
687
|
</span>
|
|
305
688
|
) : (
|
|
306
689
|
<span className="flex items-center gap-1 text-destructive">
|
|
307
|
-
<XCircle className="
|
|
308
|
-
Not available
|
|
690
|
+
<XCircle className="size-3" />
|
|
691
|
+
{t('ai_assistant.settings.notAvailable', 'Not available')}
|
|
309
692
|
</span>
|
|
310
693
|
)}
|
|
311
694
|
</p>
|
|
312
|
-
<p className="text-xs text-muted-foreground
|
|
313
|
-
{health?.search?.url
|
|
695
|
+
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
696
|
+
{health?.search?.url ?? t('ai_assistant.settings.notConfigured', 'Not configured')}
|
|
314
697
|
</p>
|
|
315
698
|
</div>
|
|
316
|
-
{health?.search?.available
|
|
317
|
-
<div className="flex
|
|
318
|
-
<svg className="
|
|
699
|
+
{health?.search?.available ? (
|
|
700
|
+
<div className="flex size-5 shrink-0 items-center justify-center rounded-full bg-status-success-bg text-status-success-icon">
|
|
701
|
+
<svg className="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
319
702
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
320
703
|
</svg>
|
|
321
704
|
</div>
|
|
322
|
-
)}
|
|
705
|
+
) : null}
|
|
323
706
|
</div>
|
|
324
707
|
</div>
|
|
325
708
|
</div>
|
|
326
709
|
|
|
327
|
-
|
|
328
|
-
<div className="mt-4 p-3 rounded-md bg-muted/30 border">
|
|
710
|
+
<div className="mt-4 rounded-md border bg-muted/30 p-3">
|
|
329
711
|
<div className="flex items-center gap-2 text-sm">
|
|
330
|
-
<Key className="
|
|
331
|
-
<span className="font-medium">
|
|
712
|
+
<Key className="size-4 text-muted-foreground" />
|
|
713
|
+
<span className="font-medium">
|
|
714
|
+
{t('ai_assistant.settings.mcpAuthLabel', 'MCP Authentication:')}
|
|
715
|
+
</span>
|
|
332
716
|
{settings?.mcpKeyConfigured ? (
|
|
333
|
-
<span className="flex items-center gap-1 text-status-success-text
|
|
334
|
-
<CheckCircle2 className="
|
|
335
|
-
MCP_SERVER_API_KEY configured
|
|
717
|
+
<span className="flex items-center gap-1 text-xs text-status-success-text">
|
|
718
|
+
<CheckCircle2 className="size-3" />
|
|
719
|
+
{t('ai_assistant.settings.mcpKeyConfigured', 'MCP_SERVER_API_KEY configured')}
|
|
336
720
|
</span>
|
|
337
721
|
) : (
|
|
338
|
-
<span className="flex items-center gap-1 text-status-warning-text
|
|
339
|
-
<XCircle className="
|
|
340
|
-
MCP_SERVER_API_KEY not set
|
|
722
|
+
<span className="flex items-center gap-1 text-xs text-status-warning-text">
|
|
723
|
+
<XCircle className="size-3" />
|
|
724
|
+
{t('ai_assistant.settings.mcpKeyMissing', 'MCP_SERVER_API_KEY not set')}
|
|
341
725
|
</span>
|
|
342
726
|
)}
|
|
343
727
|
</div>
|
|
344
|
-
<p className="text-xs text-muted-foreground
|
|
345
|
-
|
|
728
|
+
<p className="ml-6 mt-1 text-xs text-muted-foreground">
|
|
729
|
+
{t(
|
|
730
|
+
'ai_assistant.settings.mcpAuthNote',
|
|
731
|
+
'Required for AI to access platform tools via MCP server.',
|
|
732
|
+
)}
|
|
346
733
|
</p>
|
|
347
734
|
</div>
|
|
348
735
|
|
|
349
|
-
|
|
350
|
-
<div className="mt-4 p-3 rounded-md bg-muted/30 border">
|
|
736
|
+
<div className="mt-4 rounded-md border bg-muted/30 p-3">
|
|
351
737
|
<div className="flex items-center gap-2 text-sm">
|
|
352
|
-
<Server className="
|
|
353
|
-
<span className="font-medium">
|
|
354
|
-
|
|
738
|
+
<Server className="size-4 text-muted-foreground" />
|
|
739
|
+
<span className="font-medium">
|
|
740
|
+
{t('ai_assistant.settings.llmProviderLabel', 'LLM Provider:')}
|
|
741
|
+
</span>
|
|
742
|
+
<span className="font-medium">{provider?.name ?? 'Anthropic'}</span>
|
|
355
743
|
{provider?.configured ? (
|
|
356
|
-
<span className="flex items-center gap-1 text-status-success-text
|
|
357
|
-
<CheckCircle2 className="
|
|
358
|
-
{provider?.envKey
|
|
744
|
+
<span className="flex items-center gap-1 text-xs text-status-success-text">
|
|
745
|
+
<CheckCircle2 className="size-3" />
|
|
746
|
+
{provider?.envKey
|
|
747
|
+
? t('ai_assistant.settings.envKeyConfigured', '{{key}} configured', { key: provider.envKey })
|
|
748
|
+
: t('ai_assistant.settings.configured', 'Configured')}
|
|
359
749
|
</span>
|
|
360
750
|
) : (
|
|
361
|
-
<span className="flex items-center gap-1 text-status-warning-text
|
|
362
|
-
<XCircle className="
|
|
363
|
-
{provider?.envKey
|
|
751
|
+
<span className="flex items-center gap-1 text-xs text-status-warning-text">
|
|
752
|
+
<XCircle className="size-3" />
|
|
753
|
+
{t('ai_assistant.settings.envKeyMissing', '{{key}} not set', { key: provider?.envKey ?? 'ANTHROPIC_API_KEY' })}
|
|
364
754
|
</span>
|
|
365
755
|
)}
|
|
366
756
|
</div>
|
|
367
757
|
</div>
|
|
368
758
|
|
|
369
|
-
<p className="text-xs text-muted-foreground
|
|
370
|
-
|
|
759
|
+
<p className="mt-4 text-xs text-muted-foreground">
|
|
760
|
+
{t(
|
|
761
|
+
'ai_assistant.settings.meilisearchNote',
|
|
762
|
+
'Meilisearch is required for API endpoint discovery. Endpoints are indexed automatically when the MCP server starts.',
|
|
763
|
+
)}
|
|
371
764
|
</p>
|
|
372
765
|
</div>
|
|
373
766
|
|
|
374
|
-
{/* Developer Tools Section */}
|
|
375
767
|
<div className="rounded-lg border bg-card p-6">
|
|
376
|
-
<h2 className="
|
|
377
|
-
<Settings className="
|
|
378
|
-
Developer Tools
|
|
768
|
+
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold">
|
|
769
|
+
<Settings className="size-4" />
|
|
770
|
+
{t('ai_assistant.settings.developerToolsTitle', 'Developer Tools')}
|
|
379
771
|
</h2>
|
|
380
772
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
381
|
-
|
|
382
|
-
<div className="p-4 rounded-lg border bg-muted/30">
|
|
773
|
+
<div className="rounded-lg border bg-muted/30 p-4">
|
|
383
774
|
<div className="flex items-start gap-3">
|
|
384
|
-
<div className="
|
|
385
|
-
<Settings className="
|
|
775
|
+
<div className="rounded-md bg-primary/10 p-2">
|
|
776
|
+
<Settings className="size-5 text-primary" />
|
|
386
777
|
</div>
|
|
387
|
-
<div className="
|
|
388
|
-
<h3 className="text-sm font-medium">
|
|
389
|
-
|
|
390
|
-
|
|
778
|
+
<div className="min-w-0 flex-1">
|
|
779
|
+
<h3 className="text-sm font-medium">
|
|
780
|
+
{t('ai_assistant.settings.mcpConfigTitle', 'MCP Configuration')}
|
|
781
|
+
</h3>
|
|
782
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
783
|
+
{t(
|
|
784
|
+
'ai_assistant.settings.mcpConfigDescription',
|
|
785
|
+
'Generate config for Claude Code or other MCP clients.',
|
|
786
|
+
)}
|
|
391
787
|
</p>
|
|
392
788
|
<Button
|
|
393
789
|
size="sm"
|
|
@@ -395,22 +791,26 @@ function AiAssistantSettingsContent() {
|
|
|
395
791
|
className="mt-3"
|
|
396
792
|
onClick={() => setMcpConfigOpen(true)}
|
|
397
793
|
>
|
|
398
|
-
Generate MCP Config
|
|
794
|
+
{t('ai_assistant.settings.generateMcpConfig', 'Generate MCP Config')}
|
|
399
795
|
</Button>
|
|
400
796
|
</div>
|
|
401
797
|
</div>
|
|
402
798
|
</div>
|
|
403
799
|
|
|
404
|
-
|
|
405
|
-
<div className="p-4 rounded-lg border bg-muted/30">
|
|
800
|
+
<div className="rounded-lg border bg-muted/30 p-4">
|
|
406
801
|
<div className="flex items-start gap-3">
|
|
407
|
-
<div className="
|
|
408
|
-
<Key className="
|
|
802
|
+
<div className="rounded-md bg-primary/10 p-2">
|
|
803
|
+
<Key className="size-5 text-primary" />
|
|
409
804
|
</div>
|
|
410
|
-
<div className="
|
|
411
|
-
<h3 className="text-sm font-medium">
|
|
412
|
-
|
|
413
|
-
|
|
805
|
+
<div className="min-w-0 flex-1">
|
|
806
|
+
<h3 className="text-sm font-medium">
|
|
807
|
+
{t('ai_assistant.settings.sessionKeyTitle', 'Session API Key')}
|
|
808
|
+
</h3>
|
|
809
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
810
|
+
{t(
|
|
811
|
+
'ai_assistant.settings.sessionKeyDescription',
|
|
812
|
+
'Generate a temporary token for programmatic LLM access. Expires after 2 hours.',
|
|
813
|
+
)}
|
|
414
814
|
</p>
|
|
415
815
|
<Button
|
|
416
816
|
size="sm"
|
|
@@ -418,7 +818,7 @@ function AiAssistantSettingsContent() {
|
|
|
418
818
|
className="mt-3"
|
|
419
819
|
onClick={() => setSessionKeyOpen(true)}
|
|
420
820
|
>
|
|
421
|
-
Generate Session Key
|
|
821
|
+
{t('ai_assistant.settings.generateSessionKey', 'Generate Session Key')}
|
|
422
822
|
</Button>
|
|
423
823
|
</div>
|
|
424
824
|
</div>
|
|
@@ -426,44 +826,44 @@ function AiAssistantSettingsContent() {
|
|
|
426
826
|
</div>
|
|
427
827
|
</div>
|
|
428
828
|
|
|
429
|
-
{/* Dialogs */}
|
|
430
829
|
<McpConfigDialog
|
|
431
830
|
open={mcpConfigOpen}
|
|
432
831
|
onOpenChange={setMcpConfigOpen}
|
|
433
|
-
mcpUrl={health?.mcpUrl
|
|
832
|
+
mcpUrl={health?.mcpUrl ?? 'http://localhost:3001'}
|
|
434
833
|
/>
|
|
435
834
|
<SessionKeyDialog
|
|
436
835
|
open={sessionKeyOpen}
|
|
437
836
|
onOpenChange={setSessionKeyOpen}
|
|
438
837
|
/>
|
|
439
838
|
|
|
440
|
-
{/* MCP Tools Section */}
|
|
441
839
|
<div className="rounded-lg border bg-card p-6">
|
|
442
840
|
<button
|
|
443
|
-
|
|
444
|
-
|
|
841
|
+
type="button"
|
|
842
|
+
onClick={() => setToolsExpanded((prev) => !prev)}
|
|
843
|
+
className="flex w-full items-center justify-between text-left"
|
|
445
844
|
>
|
|
446
|
-
<h2 className="
|
|
447
|
-
<Wrench className="
|
|
448
|
-
MCP Tools ({tools.length}
|
|
845
|
+
<h2 className="flex items-center gap-2 text-sm font-semibold">
|
|
846
|
+
<Wrench className="size-4" />
|
|
847
|
+
{t('ai_assistant.settings.mcpToolsTitle', 'MCP Tools')} ({tools.length}{' '}
|
|
848
|
+
{t('ai_assistant.settings.mcpToolsCount', 'tools')})
|
|
449
849
|
</h2>
|
|
450
850
|
{toolsExpanded ? (
|
|
451
|
-
<ChevronDown className="
|
|
851
|
+
<ChevronDown className="size-4 text-muted-foreground" />
|
|
452
852
|
) : (
|
|
453
|
-
<ChevronRight className="
|
|
853
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
454
854
|
)}
|
|
455
855
|
</button>
|
|
456
856
|
|
|
457
|
-
{toolsExpanded
|
|
857
|
+
{toolsExpanded ? (
|
|
458
858
|
<div className="mt-4 space-y-4">
|
|
459
859
|
{Object.entries(toolsByModule).map(([module, moduleTools]) => (
|
|
460
860
|
<div key={module} className="space-y-2">
|
|
461
|
-
<h3 className="text-xs font-medium text-muted-foreground
|
|
861
|
+
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
462
862
|
{module}
|
|
463
863
|
</h3>
|
|
464
864
|
<div className="space-y-1">
|
|
465
865
|
{moduleTools.map((tool) => (
|
|
466
|
-
<div key={tool.name} className="
|
|
866
|
+
<div key={tool.name} className="border-l-2 border-muted py-1 pl-2">
|
|
467
867
|
<p className="text-sm font-medium">{tool.name}</p>
|
|
468
868
|
<p className="text-xs text-muted-foreground">{tool.description}</p>
|
|
469
869
|
</div>
|
|
@@ -472,14 +872,22 @@ function AiAssistantSettingsContent() {
|
|
|
472
872
|
</div>
|
|
473
873
|
))}
|
|
474
874
|
</div>
|
|
475
|
-
)}
|
|
875
|
+
) : null}
|
|
476
876
|
</div>
|
|
477
877
|
</div>
|
|
478
878
|
)
|
|
479
879
|
}
|
|
480
880
|
|
|
481
|
-
export function AiAssistantSettingsPageClient(
|
|
482
|
-
|
|
881
|
+
export function AiAssistantSettingsPageClient({
|
|
882
|
+
launchMode = 'selector',
|
|
883
|
+
showVisibilityControl = false,
|
|
884
|
+
}: AiAssistantSettingsPageClientProps) {
|
|
885
|
+
return (
|
|
886
|
+
<AiAssistantSettingsContent
|
|
887
|
+
launchMode={launchMode}
|
|
888
|
+
showVisibilityControl={showVisibilityControl}
|
|
889
|
+
/>
|
|
890
|
+
)
|
|
483
891
|
}
|
|
484
892
|
|
|
485
893
|
export default AiAssistantSettingsPageClient
|