Spaces:
Sleeping
Sleeping
fix: enable editing identity, sandbox, and tools in agent config UI (#148)
Browse files* fix(#140): enable editing of identity, sandbox, and tools in agent config UI
The ConfigTab's structured view only showed read-only displays for
Identity, Sandbox, and Tools sections even when in edit mode. Added
inline editing controls:
- Identity: emoji, name, theme/role inputs + identity content textarea
- Sandbox: mode/workspace dropdowns + network input
- Tools: allow/deny lists with add/remove buttons and Enter key support
Also added helper functions (updateIdentityField, updateSandboxField,
addTool, removeTool) and state for new tool entries.
Fixes #140
* fix: align sandbox edit values with agent schema
---------
Co-authored-by: Nyk <0xnykcd@googlemail.com>
src/components/panels/agent-detail-tabs.tsx
CHANGED
|
@@ -1241,6 +1241,8 @@ export function ConfigTab({
|
|
| 1241 |
const [jsonInput, setJsonInput] = useState('')
|
| 1242 |
const [availableModels, setAvailableModels] = useState<string[]>([])
|
| 1243 |
const [newFallbackModel, setNewFallbackModel] = useState('')
|
|
|
|
|
|
|
| 1244 |
|
| 1245 |
useEffect(() => {
|
| 1246 |
setConfig(agent.config || {})
|
|
@@ -1289,6 +1291,40 @@ export function ConfigTab({
|
|
| 1289 |
setNewFallbackModel('')
|
| 1290 |
}
|
| 1291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
const handleSave = async (writeToGateway: boolean = false) => {
|
| 1293 |
setSaving(true)
|
| 1294 |
setError(null)
|
|
@@ -1476,60 +1512,205 @@ export function ConfigTab({
|
|
| 1476 |
{/* Identity */}
|
| 1477 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1478 |
<h5 className="text-sm font-medium text-foreground mb-2">Identity</h5>
|
| 1479 |
-
|
| 1480 |
-
<
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1484 |
</div>
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1490 |
)}
|
| 1491 |
</div>
|
| 1492 |
|
| 1493 |
{/* Sandbox */}
|
| 1494 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1495 |
<h5 className="text-sm font-medium text-foreground mb-2">Sandbox</h5>
|
| 1496 |
-
|
| 1497 |
-
<div
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1501 |
</div>
|
| 1502 |
|
| 1503 |
{/* Tools */}
|
| 1504 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1505 |
<h5 className="text-sm font-medium text-foreground mb-2">Tools</h5>
|
| 1506 |
-
{
|
| 1507 |
-
<div className="
|
| 1508 |
-
<
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1513 |
</div>
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1523 |
</div>
|
| 1524 |
</div>
|
| 1525 |
-
)
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1533 |
)}
|
| 1534 |
</div>
|
| 1535 |
|
|
|
|
| 1241 |
const [jsonInput, setJsonInput] = useState('')
|
| 1242 |
const [availableModels, setAvailableModels] = useState<string[]>([])
|
| 1243 |
const [newFallbackModel, setNewFallbackModel] = useState('')
|
| 1244 |
+
const [newAllowTool, setNewAllowTool] = useState('')
|
| 1245 |
+
const [newDenyTool, setNewDenyTool] = useState('')
|
| 1246 |
|
| 1247 |
useEffect(() => {
|
| 1248 |
setConfig(agent.config || {})
|
|
|
|
| 1291 |
setNewFallbackModel('')
|
| 1292 |
}
|
| 1293 |
|
| 1294 |
+
const updateIdentityField = (field: string, value: string) => {
|
| 1295 |
+
setConfig((prev: any) => ({
|
| 1296 |
+
...prev,
|
| 1297 |
+
identity: { ...(prev.identity || {}), [field]: value },
|
| 1298 |
+
}))
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
const updateSandboxField = (field: string, value: string) => {
|
| 1302 |
+
setConfig((prev: any) => ({
|
| 1303 |
+
...prev,
|
| 1304 |
+
sandbox: { ...(prev.sandbox || {}), [field]: value },
|
| 1305 |
+
}))
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
const addTool = (list: 'allow' | 'deny', value: string) => {
|
| 1309 |
+
const trimmed = value.trim()
|
| 1310 |
+
if (!trimmed) return
|
| 1311 |
+
setConfig((prev: any) => {
|
| 1312 |
+
const tools = prev.tools || {}
|
| 1313 |
+
const existing = Array.isArray(tools[list]) ? tools[list] : []
|
| 1314 |
+
if (existing.includes(trimmed)) return prev
|
| 1315 |
+
return { ...prev, tools: { ...tools, [list]: [...existing, trimmed] } }
|
| 1316 |
+
})
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
const removeTool = (list: 'allow' | 'deny', index: number) => {
|
| 1320 |
+
setConfig((prev: any) => {
|
| 1321 |
+
const tools = prev.tools || {}
|
| 1322 |
+
const existing = Array.isArray(tools[list]) ? [...tools[list]] : []
|
| 1323 |
+
existing.splice(index, 1)
|
| 1324 |
+
return { ...prev, tools: { ...tools, [list]: existing } }
|
| 1325 |
+
})
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
const handleSave = async (writeToGateway: boolean = false) => {
|
| 1329 |
setSaving(true)
|
| 1330 |
setError(null)
|
|
|
|
| 1512 |
{/* Identity */}
|
| 1513 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1514 |
<h5 className="text-sm font-medium text-foreground mb-2">Identity</h5>
|
| 1515 |
+
{editing ? (
|
| 1516 |
+
<div className="space-y-3">
|
| 1517 |
+
<div className="grid grid-cols-3 gap-3">
|
| 1518 |
+
<div>
|
| 1519 |
+
<label className="block text-xs text-muted-foreground mb-1">Emoji</label>
|
| 1520 |
+
<input
|
| 1521 |
+
value={identityEmoji}
|
| 1522 |
+
onChange={(e) => updateIdentityField('emoji', e.target.value)}
|
| 1523 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm text-center focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1524 |
+
placeholder="🤖"
|
| 1525 |
+
/>
|
| 1526 |
+
</div>
|
| 1527 |
+
<div>
|
| 1528 |
+
<label className="block text-xs text-muted-foreground mb-1">Name</label>
|
| 1529 |
+
<input
|
| 1530 |
+
value={identity.name || ''}
|
| 1531 |
+
onChange={(e) => updateIdentityField('name', e.target.value)}
|
| 1532 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1533 |
+
placeholder="Agent name"
|
| 1534 |
+
/>
|
| 1535 |
+
</div>
|
| 1536 |
+
<div>
|
| 1537 |
+
<label className="block text-xs text-muted-foreground mb-1">Theme / Role</label>
|
| 1538 |
+
<input
|
| 1539 |
+
value={identity.theme || ''}
|
| 1540 |
+
onChange={(e) => updateIdentityField('theme', e.target.value)}
|
| 1541 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1542 |
+
placeholder="e.g. backend engineer"
|
| 1543 |
+
/>
|
| 1544 |
+
</div>
|
| 1545 |
+
</div>
|
| 1546 |
+
<div>
|
| 1547 |
+
<label className="block text-xs text-muted-foreground mb-1">Identity content</label>
|
| 1548 |
+
<textarea
|
| 1549 |
+
value={identity.content || ''}
|
| 1550 |
+
onChange={(e) => updateIdentityField('content', e.target.value)}
|
| 1551 |
+
rows={4}
|
| 1552 |
+
className="w-full bg-surface-1 text-foreground border border-border rounded-md px-3 py-2 font-mono text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1553 |
+
placeholder="Describe the agent's identity and personality..."
|
| 1554 |
+
/>
|
| 1555 |
+
</div>
|
| 1556 |
</div>
|
| 1557 |
+
) : (
|
| 1558 |
+
<>
|
| 1559 |
+
<div className="flex items-center gap-3 text-sm">
|
| 1560 |
+
<span className="text-2xl">{identityEmoji}</span>
|
| 1561 |
+
<div>
|
| 1562 |
+
<div className="text-foreground font-medium">{identityName}</div>
|
| 1563 |
+
<div className="text-muted-foreground">{identityTheme}</div>
|
| 1564 |
+
</div>
|
| 1565 |
+
</div>
|
| 1566 |
+
{identityPreview && (
|
| 1567 |
+
<pre className="mt-3 text-xs text-muted-foreground bg-surface-1 rounded p-2 overflow-auto whitespace-pre-wrap">
|
| 1568 |
+
{identityPreview}
|
| 1569 |
+
</pre>
|
| 1570 |
+
)}
|
| 1571 |
+
</>
|
| 1572 |
)}
|
| 1573 |
</div>
|
| 1574 |
|
| 1575 |
{/* Sandbox */}
|
| 1576 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1577 |
<h5 className="text-sm font-medium text-foreground mb-2">Sandbox</h5>
|
| 1578 |
+
{editing ? (
|
| 1579 |
+
<div className="grid grid-cols-3 gap-3">
|
| 1580 |
+
<div>
|
| 1581 |
+
<label className="block text-xs text-muted-foreground mb-1">Mode</label>
|
| 1582 |
+
<select
|
| 1583 |
+
value={sandbox.mode || ''}
|
| 1584 |
+
onChange={(e) => updateSandboxField('mode', e.target.value)}
|
| 1585 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1586 |
+
>
|
| 1587 |
+
<option value="">Not configured</option>
|
| 1588 |
+
<option value="all">All</option>
|
| 1589 |
+
<option value="non-main">Non-main</option>
|
| 1590 |
+
<option value="none">None</option>
|
| 1591 |
+
</select>
|
| 1592 |
+
</div>
|
| 1593 |
+
<div>
|
| 1594 |
+
<label className="block text-xs text-muted-foreground mb-1">Workspace Access</label>
|
| 1595 |
+
<select
|
| 1596 |
+
value={sandbox.workspaceAccess || ''}
|
| 1597 |
+
onChange={(e) => updateSandboxField('workspaceAccess', e.target.value)}
|
| 1598 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1599 |
+
>
|
| 1600 |
+
<option value="">Not configured</option>
|
| 1601 |
+
<option value="rw">Read-write</option>
|
| 1602 |
+
<option value="ro">Read-only</option>
|
| 1603 |
+
<option value="none">None</option>
|
| 1604 |
+
</select>
|
| 1605 |
+
</div>
|
| 1606 |
+
<div>
|
| 1607 |
+
<label className="block text-xs text-muted-foreground mb-1">Network</label>
|
| 1608 |
+
<input
|
| 1609 |
+
value={sandbox.network || ''}
|
| 1610 |
+
onChange={(e) => updateSandboxField('network', e.target.value)}
|
| 1611 |
+
className="w-full bg-surface-1 text-foreground rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1612 |
+
placeholder="none"
|
| 1613 |
+
/>
|
| 1614 |
+
</div>
|
| 1615 |
+
</div>
|
| 1616 |
+
) : (
|
| 1617 |
+
<div className="grid grid-cols-3 gap-2 text-sm">
|
| 1618 |
+
<div><span className="text-muted-foreground">Mode:</span> <span className="text-foreground">{sandboxMode}</span></div>
|
| 1619 |
+
<div><span className="text-muted-foreground">Workspace:</span> <span className="text-foreground">{sandboxWorkspace}</span></div>
|
| 1620 |
+
<div><span className="text-muted-foreground">Network:</span> <span className="text-foreground">{sandboxNetwork}</span></div>
|
| 1621 |
+
</div>
|
| 1622 |
+
)}
|
| 1623 |
</div>
|
| 1624 |
|
| 1625 |
{/* Tools */}
|
| 1626 |
<div className="bg-surface-1/50 rounded-lg p-4">
|
| 1627 |
<h5 className="text-sm font-medium text-foreground mb-2">Tools</h5>
|
| 1628 |
+
{editing ? (
|
| 1629 |
+
<div className="space-y-3">
|
| 1630 |
+
<div>
|
| 1631 |
+
<label className="block text-xs text-green-400 font-medium mb-1">Allow list</label>
|
| 1632 |
+
<div className="flex flex-wrap gap-1 mb-2">
|
| 1633 |
+
{toolAllow.map((tool: string, i: number) => (
|
| 1634 |
+
<span key={`${tool}-${i}`} className="px-2 py-0.5 text-xs bg-green-500/10 text-green-400 rounded border border-green-500/20 flex items-center gap-1">
|
| 1635 |
+
{tool}
|
| 1636 |
+
<button onClick={() => removeTool('allow', i)} className="text-green-400/60 hover:text-green-400 ml-1">×</button>
|
| 1637 |
+
</span>
|
| 1638 |
+
))}
|
| 1639 |
+
</div>
|
| 1640 |
+
<div className="flex gap-2">
|
| 1641 |
+
<input
|
| 1642 |
+
value={newAllowTool}
|
| 1643 |
+
onChange={(e) => setNewAllowTool(e.target.value)}
|
| 1644 |
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTool('allow', newAllowTool); setNewAllowTool('') } }}
|
| 1645 |
+
placeholder="Add allowed tool name"
|
| 1646 |
+
className="flex-1 bg-surface-1 text-foreground rounded px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1647 |
+
/>
|
| 1648 |
+
<button
|
| 1649 |
+
onClick={() => { addTool('allow', newAllowTool); setNewAllowTool('') }}
|
| 1650 |
+
className="px-3 py-2 text-xs bg-green-500/20 text-green-400 border border-green-500/30 rounded hover:bg-green-500/30 transition-smooth"
|
| 1651 |
+
>
|
| 1652 |
+
Add
|
| 1653 |
+
</button>
|
| 1654 |
+
</div>
|
| 1655 |
</div>
|
| 1656 |
+
<div>
|
| 1657 |
+
<label className="block text-xs text-red-400 font-medium mb-1">Deny list</label>
|
| 1658 |
+
<div className="flex flex-wrap gap-1 mb-2">
|
| 1659 |
+
{toolDeny.map((tool: string, i: number) => (
|
| 1660 |
+
<span key={`${tool}-${i}`} className="px-2 py-0.5 text-xs bg-red-500/10 text-red-400 rounded border border-red-500/20 flex items-center gap-1">
|
| 1661 |
+
{tool}
|
| 1662 |
+
<button onClick={() => removeTool('deny', i)} className="text-red-400/60 hover:text-red-400 ml-1">×</button>
|
| 1663 |
+
</span>
|
| 1664 |
+
))}
|
| 1665 |
+
</div>
|
| 1666 |
+
<div className="flex gap-2">
|
| 1667 |
+
<input
|
| 1668 |
+
value={newDenyTool}
|
| 1669 |
+
onChange={(e) => setNewDenyTool(e.target.value)}
|
| 1670 |
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTool('deny', newDenyTool); setNewDenyTool('') } }}
|
| 1671 |
+
placeholder="Add denied tool name"
|
| 1672 |
+
className="flex-1 bg-surface-1 text-foreground rounded px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
| 1673 |
+
/>
|
| 1674 |
+
<button
|
| 1675 |
+
onClick={() => { addTool('deny', newDenyTool); setNewDenyTool('') }}
|
| 1676 |
+
className="px-3 py-2 text-xs bg-red-500/20 text-red-400 border border-red-500/30 rounded hover:bg-red-500/30 transition-smooth"
|
| 1677 |
+
>
|
| 1678 |
+
Add
|
| 1679 |
+
</button>
|
| 1680 |
+
</div>
|
| 1681 |
</div>
|
| 1682 |
</div>
|
| 1683 |
+
) : (
|
| 1684 |
+
<>
|
| 1685 |
+
{toolAllow.length > 0 && (
|
| 1686 |
+
<div className="mb-2">
|
| 1687 |
+
<span className="text-xs text-green-400 font-medium">Allow ({toolAllow.length}):</span>
|
| 1688 |
+
<div className="flex flex-wrap gap-1 mt-1">
|
| 1689 |
+
{toolAllow.map((tool: string) => (
|
| 1690 |
+
<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>
|
| 1691 |
+
))}
|
| 1692 |
+
</div>
|
| 1693 |
+
</div>
|
| 1694 |
+
)}
|
| 1695 |
+
{toolDeny.length > 0 && (
|
| 1696 |
+
<div>
|
| 1697 |
+
<span className="text-xs text-red-400 font-medium">Deny ({toolDeny.length}):</span>
|
| 1698 |
+
<div className="flex flex-wrap gap-1 mt-1">
|
| 1699 |
+
{toolDeny.map((tool: string) => (
|
| 1700 |
+
<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>
|
| 1701 |
+
))}
|
| 1702 |
+
</div>
|
| 1703 |
+
</div>
|
| 1704 |
+
)}
|
| 1705 |
+
{toolAllow.length === 0 && toolDeny.length === 0 && !toolRawPreview && (
|
| 1706 |
+
<div className="text-xs text-muted-foreground">No tools configured</div>
|
| 1707 |
+
)}
|
| 1708 |
+
{toolRawPreview && (
|
| 1709 |
+
<pre className="mt-3 text-xs text-muted-foreground bg-surface-1 rounded p-2 overflow-auto whitespace-pre-wrap">
|
| 1710 |
+
{toolRawPreview}
|
| 1711 |
+
</pre>
|
| 1712 |
+
)}
|
| 1713 |
+
</>
|
| 1714 |
)}
|
| 1715 |
</div>
|
| 1716 |
|