nyk commited on
Commit
2cf3427
·
unverified ·
1 Parent(s): 5be7cdc

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
- <div className="text-sm">
1287
- <div><span className="text-muted-foreground">Primary:</span> <span className="text-foreground font-mono">{model.primary || 'N/A'}</span></div>
1288
- {model.fallbacks && model.fallbacks.length > 0 && (
1289
- <div className="mt-1">
1290
- <span className="text-muted-foreground">Fallbacks:</span>
1291
- <div className="flex flex-wrap gap-1 mt-1">
1292
- {model.fallbacks.map((fb: string, i: number) => (
1293
- <span key={i} className="px-2 py-0.5 text-xs bg-surface-2 rounded text-muted-foreground font-mono">{fb.split('/').pop()}</span>
 
 
 
 
 
 
1294
  ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1295
  </div>
1296
  </div>
1297
- )}
1298
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">{identity.emoji || '?'}</span>
1306
  <div>
1307
- <div className="text-foreground font-medium">{identity.name || 'N/A'}</div>
1308
- <div className="text-muted-foreground">{identity.theme || 'N/A'}</div>
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">{sandbox.mode || 'N/A'}</span></div>
1318
- <div><span className="text-muted-foreground">Workspace:</span> <span className="text-foreground">{sandbox.workspaceAccess || 'N/A'}</span></div>
1319
- <div><span className="text-muted-foreground">Network:</span> <span className="text-foreground">{sandbox.docker?.network || 'none'}</span></div>
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
- {tools.allow && tools.allow.length > 0 && (
1327
  <div className="mb-2">
1328
- <span className="text-xs text-green-400 font-medium">Allow ({tools.allow.length}):</span>
1329
  <div className="flex flex-wrap gap-1 mt-1">
1330
- {tools.allow.map((tool: string) => (
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
- {tools.deny && tools.deny.length > 0 && (
1337
  <div>
1338
- <span className="text-xs text-red-400 font-medium">Deny ({tools.deny.length}):</span>
1339
  <div className="flex flex-wrap gap-1 mt-1">
1340
- {tools.deny.map((tool: string) => (
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 */}