Spaces:
Runtime error
Runtime error
nyk commited on
feat: improve agent config tab model editing and display
Browse files
src/components/panels/agent-detail-tabs.tsx
CHANGED
|
@@ -1189,16 +1189,66 @@ export function ConfigTab({
|
|
| 1189 |
const [saving, setSaving] = useState(false)
|
| 1190 |
const [error, setError] = useState<string | null>(null)
|
| 1191 |
const [jsonInput, setJsonInput] = useState('')
|
|
|
|
|
|
|
| 1192 |
|
| 1193 |
useEffect(() => {
|
| 1194 |
setConfig(agent.config || {})
|
| 1195 |
setJsonInput(JSON.stringify(agent.config || {}, null, 2))
|
| 1196 |
}, [agent.config])
|
| 1197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
const handleSave = async (writeToGateway: boolean = false) => {
|
| 1199 |
setSaving(true)
|
| 1200 |
setError(null)
|
| 1201 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1202 |
const response = await fetch(`/api/agents/${agent.id}`, {
|
| 1203 |
method: 'PUT',
|
| 1204 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1225,6 +1275,18 @@ export function ConfigTab({
|
|
| 1225 |
const tools = config.tools || {}
|
| 1226 |
const subagents = config.subagents || {}
|
| 1227 |
const memorySearch = config.memorySearch || {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1228 |
|
| 1229 |
return (
|
| 1230 |
<div className="p-6 space-y-4">
|
|
@@ -1283,66 +1345,142 @@ export function ConfigTab({
|
|
| 1283 |
{/* Model */}
|
| 1284 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1285 |
<h5 className="text-sm font-medium text-foreground mb-2">Model</h5>
|
| 1286 |
-
|
| 1287 |
-
<div
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
<
|
| 1291 |
-
|
| 1292 |
-
{
|
| 1293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1294 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1295 |
</div>
|
| 1296 |
</div>
|
| 1297 |
-
|
| 1298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1299 |
</div>
|
| 1300 |
|
| 1301 |
{/* Identity */}
|
| 1302 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1303 |
<h5 className="text-sm font-medium text-foreground mb-2">Identity</h5>
|
| 1304 |
<div className="flex items-center gap-3 text-sm">
|
| 1305 |
-
<span className="text-2xl">{
|
| 1306 |
<div>
|
| 1307 |
-
<div className="text-foreground font-medium">{
|
| 1308 |
-
<div className="text-muted-foreground">{
|
| 1309 |
</div>
|
| 1310 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1311 |
</div>
|
| 1312 |
|
| 1313 |
{/* Sandbox */}
|
| 1314 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1315 |
<h5 className="text-sm font-medium text-foreground mb-2">Sandbox</h5>
|
| 1316 |
<div className="grid grid-cols-3 gap-2 text-sm">
|
| 1317 |
-
<div><span className="text-muted-foreground">Mode:</span> <span className="text-foreground">{
|
| 1318 |
-
<div><span className="text-muted-foreground">Workspace:</span> <span className="text-foreground">{
|
| 1319 |
-
<div><span className="text-muted-foreground">Network:</span> <span className="text-foreground">{
|
| 1320 |
</div>
|
| 1321 |
</div>
|
| 1322 |
|
| 1323 |
{/* Tools */}
|
| 1324 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1325 |
<h5 className="text-sm font-medium text-foreground mb-2">Tools</h5>
|
| 1326 |
-
{
|
| 1327 |
<div className="mb-2">
|
| 1328 |
-
<span className="text-xs text-green-400 font-medium">Allow ({
|
| 1329 |
<div className="flex flex-wrap gap-1 mt-1">
|
| 1330 |
-
{
|
| 1331 |
<span key={tool} className="px-2 py-0.5 text-xs bg-green-500/10 text-green-400 rounded border border-green-500/20">{tool}</span>
|
| 1332 |
))}
|
| 1333 |
</div>
|
| 1334 |
</div>
|
| 1335 |
)}
|
| 1336 |
-
{
|
| 1337 |
<div>
|
| 1338 |
-
<span className="text-xs text-red-400 font-medium">Deny ({
|
| 1339 |
<div className="flex flex-wrap gap-1 mt-1">
|
| 1340 |
-
{
|
| 1341 |
<span key={tool} className="px-2 py-0.5 text-xs bg-red-500/10 text-red-400 rounded border border-red-500/20">{tool}</span>
|
| 1342 |
))}
|
| 1343 |
</div>
|
| 1344 |
</div>
|
| 1345 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
</div>
|
| 1347 |
|
| 1348 |
{/* Subagents */}
|
|
|
|
| 1189 |
const [saving, setSaving] = useState(false)
|
| 1190 |
const [error, setError] = useState<string | null>(null)
|
| 1191 |
const [jsonInput, setJsonInput] = useState('')
|
| 1192 |
+
const [availableModels, setAvailableModels] = useState<string[]>([])
|
| 1193 |
+
const [newFallbackModel, setNewFallbackModel] = useState('')
|
| 1194 |
|
| 1195 |
useEffect(() => {
|
| 1196 |
setConfig(agent.config || {})
|
| 1197 |
setJsonInput(JSON.stringify(agent.config || {}, null, 2))
|
| 1198 |
}, [agent.config])
|
| 1199 |
|
| 1200 |
+
useEffect(() => {
|
| 1201 |
+
const loadAvailableModels = async () => {
|
| 1202 |
+
try {
|
| 1203 |
+
const response = await fetch('/api/status?action=models')
|
| 1204 |
+
if (!response.ok) return
|
| 1205 |
+
const data = await response.json()
|
| 1206 |
+
const models = Array.isArray(data.models) ? data.models : []
|
| 1207 |
+
const names = models
|
| 1208 |
+
.map((model: any) => String(model.name || model.alias || '').trim())
|
| 1209 |
+
.filter(Boolean)
|
| 1210 |
+
setAvailableModels(Array.from(new Set<string>(names)))
|
| 1211 |
+
} catch {
|
| 1212 |
+
// Ignore model suggestions if unavailable.
|
| 1213 |
+
}
|
| 1214 |
+
}
|
| 1215 |
+
loadAvailableModels()
|
| 1216 |
+
}, [])
|
| 1217 |
+
|
| 1218 |
+
const updateModelConfig = (updater: (current: { primary?: string; fallbacks?: string[] }) => { primary?: string; fallbacks?: string[] }) => {
|
| 1219 |
+
setConfig((prev: any) => {
|
| 1220 |
+
const nextModel = updater({ ...(prev?.model || {}) })
|
| 1221 |
+
const dedupedFallbacks = [...new Set((nextModel.fallbacks || []).map((value) => value.trim()).filter(Boolean))]
|
| 1222 |
+
return {
|
| 1223 |
+
...prev,
|
| 1224 |
+
model: {
|
| 1225 |
+
...nextModel,
|
| 1226 |
+
fallbacks: dedupedFallbacks,
|
| 1227 |
+
},
|
| 1228 |
+
}
|
| 1229 |
+
})
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
const addFallbackModel = () => {
|
| 1233 |
+
const trimmed = newFallbackModel.trim()
|
| 1234 |
+
if (!trimmed) return
|
| 1235 |
+
updateModelConfig((current) => ({
|
| 1236 |
+
...current,
|
| 1237 |
+
fallbacks: [...(current.fallbacks || []), trimmed],
|
| 1238 |
+
}))
|
| 1239 |
+
setNewFallbackModel('')
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
const handleSave = async (writeToGateway: boolean = false) => {
|
| 1243 |
setSaving(true)
|
| 1244 |
setError(null)
|
| 1245 |
try {
|
| 1246 |
+
if (!showJson) {
|
| 1247 |
+
const primary = String(config?.model?.primary || '').trim()
|
| 1248 |
+
if (!primary) {
|
| 1249 |
+
throw new Error('Primary model is required')
|
| 1250 |
+
}
|
| 1251 |
+
}
|
| 1252 |
const response = await fetch(`/api/agents/${agent.id}`, {
|
| 1253 |
method: 'PUT',
|
| 1254 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 1275 |
const tools = config.tools || {}
|
| 1276 |
const subagents = config.subagents || {}
|
| 1277 |
const memorySearch = config.memorySearch || {}
|
| 1278 |
+
const sandboxMode = sandbox.mode || sandbox.sandboxMode || sandbox.sandbox_mode || config.sandboxMode || 'not configured'
|
| 1279 |
+
const sandboxWorkspace = sandbox.workspaceAccess || sandbox.workspace_access || sandbox.workspace || config.workspaceAccess || 'not configured'
|
| 1280 |
+
const sandboxNetwork = sandbox?.docker?.network || sandbox.network || sandbox.dockerNetwork || sandbox.docker_network || 'none'
|
| 1281 |
+
const identityName = identity.name || agent.name || 'not configured'
|
| 1282 |
+
const identityTheme = identity.theme || agent.role || 'not configured'
|
| 1283 |
+
const identityEmoji = identity.emoji || '?'
|
| 1284 |
+
const identityPreview = identity.content || ''
|
| 1285 |
+
const toolAllow = Array.isArray(tools.allow) ? tools.allow : []
|
| 1286 |
+
const toolDeny = Array.isArray(tools.deny) ? tools.deny : []
|
| 1287 |
+
const toolRawPreview = typeof tools.raw === 'string' ? tools.raw : ''
|
| 1288 |
+
const modelPrimary = model.primary || ''
|
| 1289 |
+
const modelFallbacks = Array.isArray(model.fallbacks) ? model.fallbacks : []
|
| 1290 |
|
| 1291 |
return (
|
| 1292 |
<div className="p-6 space-y-4">
|
|
|
|
| 1345 |
{/* Model */}
|
| 1346 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1347 |
<h5 className="text-sm font-medium text-foreground mb-2">Model</h5>
|
| 1348 |
+
{editing ? (
|
| 1349 |
+
<div className="space-y-3">
|
| 1350 |
+
<div>
|
| 1351 |
+
<label className="block text-xs text-muted-foreground mb-1">Primary model</label>
|
| 1352 |
+
<input
|
| 1353 |
+
value={modelPrimary}
|
| 1354 |
+
onChange={(e) => updateModelConfig((current) => ({ ...current, primary: e.target.value }))}
|
| 1355 |
+
list="agent-model-suggestions"
|
| 1356 |
+
placeholder="anthropic/claude-sonnet-4-20250514"
|
| 1357 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1358 |
+
/>
|
| 1359 |
+
<datalist id="agent-model-suggestions">
|
| 1360 |
+
{availableModels.map((name) => (
|
| 1361 |
+
<option key={name} value={name} />
|
| 1362 |
))}
|
| 1363 |
+
</datalist>
|
| 1364 |
+
</div>
|
| 1365 |
+
<div>
|
| 1366 |
+
<label className="block text-xs text-muted-foreground mb-1">Fallback models</label>
|
| 1367 |
+
<div className="space-y-2">
|
| 1368 |
+
{modelFallbacks.map((fallback: string, index: number) => (
|
| 1369 |
+
<div key={`${fallback}-${index}`} className="flex gap-2">
|
| 1370 |
+
<input
|
| 1371 |
+
value={fallback}
|
| 1372 |
+
onChange={(e) => {
|
| 1373 |
+
const next = [...modelFallbacks]
|
| 1374 |
+
next[index] = e.target.value
|
| 1375 |
+
updateModelConfig((current) => ({ ...current, fallbacks: next }))
|
| 1376 |
+
}}
|
| 1377 |
+
list="agent-model-suggestions"
|
| 1378 |
+
className="flex-1 bg-surface-1 text-foreground rounded px-3 py-2 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1379 |
+
/>
|
| 1380 |
+
<button
|
| 1381 |
+
onClick={() => {
|
| 1382 |
+
const next = modelFallbacks.filter((_: string, i: number) => i !== index)
|
| 1383 |
+
updateModelConfig((current) => ({ ...current, fallbacks: next }))
|
| 1384 |
+
}}
|
| 1385 |
+
className="px-3 py-2 text-xs bg-red-500/10 text-red-400 border border-red-500/30 rounded hover:bg-red-500/20 transition-smooth"
|
| 1386 |
+
>
|
| 1387 |
+
Remove
|
| 1388 |
+
</button>
|
| 1389 |
+
</div>
|
| 1390 |
+
))}
|
| 1391 |
+
<div className="flex gap-2">
|
| 1392 |
+
<input
|
| 1393 |
+
value={newFallbackModel}
|
| 1394 |
+
onChange={(e) => setNewFallbackModel(e.target.value)}
|
| 1395 |
+
list="agent-model-suggestions"
|
| 1396 |
+
placeholder="Add fallback model"
|
| 1397 |
+
className="flex-1 bg-surface-1 text-foreground rounded px-3 py-2 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1398 |
+
/>
|
| 1399 |
+
<button
|
| 1400 |
+
onClick={addFallbackModel}
|
| 1401 |
+
className="px-3 py-2 text-xs bg-secondary text-foreground rounded hover:bg-surface-2 transition-smooth"
|
| 1402 |
+
>
|
| 1403 |
+
Add
|
| 1404 |
+
</button>
|
| 1405 |
+
</div>
|
| 1406 |
</div>
|
| 1407 |
</div>
|
| 1408 |
+
</div>
|
| 1409 |
+
) : (
|
| 1410 |
+
<div className="text-sm">
|
| 1411 |
+
<div><span className="text-muted-foreground">Primary:</span> <span className="text-foreground font-mono">{modelPrimary || 'not configured'}</span></div>
|
| 1412 |
+
{modelFallbacks.length > 0 && (
|
| 1413 |
+
<div className="mt-1">
|
| 1414 |
+
<span className="text-muted-foreground">Fallbacks:</span>
|
| 1415 |
+
<div className="flex flex-wrap gap-1 mt-1">
|
| 1416 |
+
{modelFallbacks.map((fb: string, i: number) => (
|
| 1417 |
+
<span key={i} className="px-2 py-0.5 text-xs bg-surface-2 rounded text-muted-foreground font-mono">{fb.split('/').pop()}</span>
|
| 1418 |
+
))}
|
| 1419 |
+
</div>
|
| 1420 |
+
</div>
|
| 1421 |
+
)}
|
| 1422 |
+
</div>
|
| 1423 |
+
)}
|
| 1424 |
</div>
|
| 1425 |
|
| 1426 |
{/* Identity */}
|
| 1427 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1428 |
<h5 className="text-sm font-medium text-foreground mb-2">Identity</h5>
|
| 1429 |
<div className="flex items-center gap-3 text-sm">
|
| 1430 |
+
<span className="text-2xl">{identityEmoji}</span>
|
| 1431 |
<div>
|
| 1432 |
+
<div className="text-foreground font-medium">{identityName}</div>
|
| 1433 |
+
<div className="text-muted-foreground">{identityTheme}</div>
|
| 1434 |
</div>
|
| 1435 |
</div>
|
| 1436 |
+
{identityPreview && (
|
| 1437 |
+
<pre className="mt-3 text-xs text-muted-foreground bg-surface-1 rounded p-2 overflow-auto whitespace-pre-wrap">
|
| 1438 |
+
{identityPreview}
|
| 1439 |
+
</pre>
|
| 1440 |
+
)}
|
| 1441 |
</div>
|
| 1442 |
|
| 1443 |
{/* Sandbox */}
|
| 1444 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1445 |
<h5 className="text-sm font-medium text-foreground mb-2">Sandbox</h5>
|
| 1446 |
<div className="grid grid-cols-3 gap-2 text-sm">
|
| 1447 |
+
<div><span className="text-muted-foreground">Mode:</span> <span className="text-foreground">{sandboxMode}</span></div>
|
| 1448 |
+
<div><span className="text-muted-foreground">Workspace:</span> <span className="text-foreground">{sandboxWorkspace}</span></div>
|
| 1449 |
+
<div><span className="text-muted-foreground">Network:</span> <span className="text-foreground">{sandboxNetwork}</span></div>
|
| 1450 |
</div>
|
| 1451 |
</div>
|
| 1452 |
|
| 1453 |
{/* Tools */}
|
| 1454 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1455 |
<h5 className="text-sm font-medium text-foreground mb-2">Tools</h5>
|
| 1456 |
+
{toolAllow.length > 0 && (
|
| 1457 |
<div className="mb-2">
|
| 1458 |
+
<span className="text-xs text-green-400 font-medium">Allow ({toolAllow.length}):</span>
|
| 1459 |
<div className="flex flex-wrap gap-1 mt-1">
|
| 1460 |
+
{toolAllow.map((tool: string) => (
|
| 1461 |
<span key={tool} className="px-2 py-0.5 text-xs bg-green-500/10 text-green-400 rounded border border-green-500/20">{tool}</span>
|
| 1462 |
))}
|
| 1463 |
</div>
|
| 1464 |
</div>
|
| 1465 |
)}
|
| 1466 |
+
{toolDeny.length > 0 && (
|
| 1467 |
<div>
|
| 1468 |
+
<span className="text-xs text-red-400 font-medium">Deny ({toolDeny.length}):</span>
|
| 1469 |
<div className="flex flex-wrap gap-1 mt-1">
|
| 1470 |
+
{toolDeny.map((tool: string) => (
|
| 1471 |
<span key={tool} className="px-2 py-0.5 text-xs bg-red-500/10 text-red-400 rounded border border-red-500/20">{tool}</span>
|
| 1472 |
))}
|
| 1473 |
</div>
|
| 1474 |
</div>
|
| 1475 |
)}
|
| 1476 |
+
{toolAllow.length === 0 && toolDeny.length === 0 && !toolRawPreview && (
|
| 1477 |
+
<div className="text-xs text-muted-foreground">No tools configured</div>
|
| 1478 |
+
)}
|
| 1479 |
+
{toolRawPreview && (
|
| 1480 |
+
<pre className="mt-3 text-xs text-muted-foreground bg-surface-1 rounded p-2 overflow-auto whitespace-pre-wrap">
|
| 1481 |
+
{toolRawPreview}
|
| 1482 |
+
</pre>
|
| 1483 |
+
)}
|
| 1484 |
</div>
|
| 1485 |
|
| 1486 |
{/* Subagents */}
|