@swarmclawai/swarmclaw 0.4.5 → 0.5.0
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/README.md +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { Agent } from '@/types'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { AgentFilesEditor } from './agent-files-editor'
|
|
7
|
+
import { OpenClawSkillsPanel } from './openclaw-skills-panel'
|
|
8
|
+
import { PermissionPresetSelector } from './permission-preset-selector'
|
|
9
|
+
import { ExecConfigPanel } from './exec-config-panel'
|
|
10
|
+
import { SandboxEnvPanel } from './sandbox-env-panel'
|
|
11
|
+
import { CronJobForm } from './cron-job-form'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
agent: Agent
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type InspectorTab = 'overview' | 'files' | 'skills' | 'automations' | 'advanced'
|
|
18
|
+
|
|
19
|
+
const TABS: { id: InspectorTab; label: string; openclawOnly?: boolean }[] = [
|
|
20
|
+
{ id: 'overview', label: 'Overview' },
|
|
21
|
+
{ id: 'files', label: 'Files', openclawOnly: true },
|
|
22
|
+
{ id: 'skills', label: 'Skills' },
|
|
23
|
+
{ id: 'automations', label: 'Automations' },
|
|
24
|
+
{ id: 'advanced', label: 'Advanced' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export function InspectorPanel({ agent }: Props) {
|
|
28
|
+
const inspectorTab = useAppStore((s) => s.inspectorTab)
|
|
29
|
+
const setInspectorTab = useAppStore((s) => s.setInspectorTab)
|
|
30
|
+
const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
|
|
31
|
+
const schedules = useAppStore((s) => s.schedules)
|
|
32
|
+
|
|
33
|
+
const isOpenClaw = agent.provider === 'openclaw'
|
|
34
|
+
const visibleTabs = TABS.filter((t) => !t.openclawOnly || isOpenClaw)
|
|
35
|
+
|
|
36
|
+
// Reset to overview if current tab is not visible
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!visibleTabs.find((t) => t.id === inspectorTab)) {
|
|
39
|
+
setInspectorTab('overview')
|
|
40
|
+
}
|
|
41
|
+
}, [isOpenClaw]) // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
|
|
43
|
+
// Close on Escape
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handler = (e: KeyboardEvent) => {
|
|
46
|
+
if (e.key === 'Escape') setInspectorOpen(false)
|
|
47
|
+
}
|
|
48
|
+
window.addEventListener('keydown', handler)
|
|
49
|
+
return () => window.removeEventListener('keydown', handler)
|
|
50
|
+
}, [setInspectorOpen])
|
|
51
|
+
|
|
52
|
+
const agentSchedules = Object.values(schedules).filter((s) => s.agentId === agent.id)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden">
|
|
56
|
+
{/* Header */}
|
|
57
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
58
|
+
<h3 className="font-display text-[14px] font-600 text-text truncate">{agent.name}</h3>
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => setInspectorOpen(false)}
|
|
61
|
+
className="p-1 rounded-[6px] text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer transition-all hover:bg-white/[0.04]"
|
|
62
|
+
aria-label="Close inspector"
|
|
63
|
+
>
|
|
64
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
65
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
66
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
67
|
+
</svg>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Tab bar */}
|
|
72
|
+
<div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0">
|
|
73
|
+
{visibleTabs.map((tab) => (
|
|
74
|
+
<button
|
|
75
|
+
key={tab.id}
|
|
76
|
+
onClick={() => setInspectorTab(tab.id)}
|
|
77
|
+
className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap
|
|
78
|
+
${inspectorTab === tab.id
|
|
79
|
+
? 'bg-accent-soft text-accent-bright'
|
|
80
|
+
: 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
81
|
+
style={{ fontFamily: 'inherit' }}
|
|
82
|
+
>
|
|
83
|
+
{tab.label}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Tab content */}
|
|
89
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
90
|
+
{inspectorTab === 'overview' && (
|
|
91
|
+
<OverviewTab agent={agent} />
|
|
92
|
+
)}
|
|
93
|
+
{inspectorTab === 'files' && isOpenClaw && (
|
|
94
|
+
<AgentFilesEditor agentId={agent.id} />
|
|
95
|
+
)}
|
|
96
|
+
{inspectorTab === 'skills' && (
|
|
97
|
+
isOpenClaw ? (
|
|
98
|
+
<OpenClawSkillsPanel
|
|
99
|
+
agentId={agent.id}
|
|
100
|
+
initialMode={agent.openclawSkillMode}
|
|
101
|
+
initialAllowed={agent.openclawAllowedSkills}
|
|
102
|
+
/>
|
|
103
|
+
) : (
|
|
104
|
+
<div className="p-4 text-[13px] text-text-3/50">
|
|
105
|
+
Local skills are configured in the agent editor.
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
)}
|
|
109
|
+
{inspectorTab === 'automations' && (
|
|
110
|
+
<AutomationsTab schedules={agentSchedules} agent={agent} />
|
|
111
|
+
)}
|
|
112
|
+
{inspectorTab === 'advanced' && (
|
|
113
|
+
<AdvancedTab agent={agent} />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function OverviewTab({ agent }: { agent: Agent }) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="p-4 flex flex-col gap-4">
|
|
123
|
+
<div>
|
|
124
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Description</label>
|
|
125
|
+
<p className="text-[13px] text-text-2">{agent.description || 'No description'}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Provider / Model</label>
|
|
129
|
+
<p className="text-[13px] text-text-2 font-mono">{agent.provider} / {agent.model || 'default'}</p>
|
|
130
|
+
</div>
|
|
131
|
+
{agent.systemPrompt && (
|
|
132
|
+
<div>
|
|
133
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">System Prompt</label>
|
|
134
|
+
<p className="text-[12px] text-text-3 bg-white/[0.02] rounded-[8px] p-2.5 border border-white/[0.04] max-h-[200px] overflow-y-auto whitespace-pre-wrap font-mono">
|
|
135
|
+
{agent.systemPrompt}
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
{agent.capabilities && agent.capabilities.length > 0 && (
|
|
140
|
+
<div>
|
|
141
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Capabilities</label>
|
|
142
|
+
<div className="flex flex-wrap gap-1">
|
|
143
|
+
{agent.capabilities.map((cap) => (
|
|
144
|
+
<span key={cap} className="px-2 py-0.5 rounded-[6px] text-[11px] font-600 bg-accent-soft text-accent-bright">
|
|
145
|
+
{cap}
|
|
146
|
+
</span>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{agent.tools && agent.tools.length > 0 && (
|
|
152
|
+
<div>
|
|
153
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Tools</label>
|
|
154
|
+
<div className="flex flex-wrap gap-1">
|
|
155
|
+
{agent.tools.map((tool) => (
|
|
156
|
+
<span key={tool} className="px-2 py-0.5 rounded-[6px] text-[11px] font-600 bg-sky-400/[0.08] text-sky-400/70">
|
|
157
|
+
{tool}
|
|
158
|
+
</span>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function AutomationsTab({ schedules, agent }: { schedules: Array<{ id: string; name: string; status: string; cron?: string; scheduleType: string }>; agent: Agent }) {
|
|
168
|
+
const isOpenClaw = agent.provider === 'openclaw'
|
|
169
|
+
const [gatewayCrons, setGatewayCrons] = useState<Array<{ id: string; name: string; enabled: boolean; schedule?: { kind: string; value: string }; state?: { nextRun?: string; lastRun?: string } }>>([])
|
|
170
|
+
const [cronLoading, setCronLoading] = useState(false)
|
|
171
|
+
const [showCronForm, setShowCronForm] = useState(false)
|
|
172
|
+
|
|
173
|
+
const loadCrons = useCallback(async () => {
|
|
174
|
+
if (!isOpenClaw) return
|
|
175
|
+
setCronLoading(true)
|
|
176
|
+
try {
|
|
177
|
+
const { api } = await import('@/lib/api-client')
|
|
178
|
+
const crons = await api<Array<{ id: string; name: string; enabled: boolean; schedule?: { kind: string; value: string }; state?: { nextRun?: string; lastRun?: string } }>>('GET', '/openclaw/cron')
|
|
179
|
+
setGatewayCrons(crons.filter((c) => (c as Record<string, unknown>).agentId === agent.id))
|
|
180
|
+
} catch { /* ignore */ }
|
|
181
|
+
finally { setCronLoading(false) }
|
|
182
|
+
}, [isOpenClaw, agent.id])
|
|
183
|
+
|
|
184
|
+
useEffect(() => { loadCrons() }, [loadCrons])
|
|
185
|
+
|
|
186
|
+
const handleRunCron = async (id: string) => {
|
|
187
|
+
try {
|
|
188
|
+
const { api } = await import('@/lib/api-client')
|
|
189
|
+
await api('POST', '/openclaw/cron', { action: 'run', id })
|
|
190
|
+
} catch { /* ignore */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const handleRemoveCron = async (id: string) => {
|
|
194
|
+
try {
|
|
195
|
+
const { api } = await import('@/lib/api-client')
|
|
196
|
+
await api('POST', '/openclaw/cron', { action: 'remove', id })
|
|
197
|
+
setGatewayCrons((prev) => prev.filter((c) => c.id !== id))
|
|
198
|
+
} catch { /* ignore */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="p-4 flex flex-col gap-3">
|
|
203
|
+
{/* Local schedules */}
|
|
204
|
+
{schedules.map((s) => (
|
|
205
|
+
<div key={s.id} className="py-2 px-3 rounded-[10px] bg-white/[0.02] border border-white/[0.04]">
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<span className="text-[13px] font-600 text-text truncate flex-1">{s.name}</span>
|
|
208
|
+
<span className={`text-[10px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px]
|
|
209
|
+
${s.status === 'active' ? 'text-emerald-400 bg-emerald-400/[0.08]' : 'text-text-3/50 bg-white/[0.02]'}`}>
|
|
210
|
+
{s.status}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div className="text-[11px] text-text-3/50 mt-1">
|
|
214
|
+
{s.scheduleType}{s.cron ? ` (${s.cron})` : ''}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
218
|
+
|
|
219
|
+
{/* Gateway cron jobs */}
|
|
220
|
+
{isOpenClaw && (
|
|
221
|
+
<>
|
|
222
|
+
{cronLoading && <div className="text-[12px] text-text-3/50">Loading gateway crons...</div>}
|
|
223
|
+
{gatewayCrons.map((c) => (
|
|
224
|
+
<div key={c.id} className="py-2 px-3 rounded-[10px] bg-white/[0.02] border border-white/[0.04]">
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<span className="text-[13px] font-600 text-text truncate flex-1">{c.name}</span>
|
|
227
|
+
<span className={`text-[10px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px]
|
|
228
|
+
${c.enabled ? 'text-emerald-400 bg-emerald-400/[0.08]' : 'text-text-3/50 bg-white/[0.02]'}`}>
|
|
229
|
+
{c.enabled ? 'active' : 'disabled'}
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="text-[11px] text-text-3/50 mt-1">
|
|
233
|
+
{c.schedule?.kind} {c.schedule?.value}
|
|
234
|
+
{c.state?.nextRun && ` — next: ${c.state.nextRun}`}
|
|
235
|
+
</div>
|
|
236
|
+
<div className="flex gap-2 mt-2">
|
|
237
|
+
<button onClick={() => handleRunCron(c.id)} className="text-[10px] text-accent-bright bg-transparent border-none cursor-pointer hover:underline">Run Now</button>
|
|
238
|
+
<button onClick={() => handleRemoveCron(c.id)} className="text-[10px] text-red-400/70 bg-transparent border-none cursor-pointer hover:underline">Delete</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
{showCronForm ? (
|
|
243
|
+
<CronJobForm agentId={agent.id} onSaved={() => { setShowCronForm(false); loadCrons() }} onCancel={() => setShowCronForm(false)} />
|
|
244
|
+
) : (
|
|
245
|
+
<button
|
|
246
|
+
onClick={() => setShowCronForm(true)}
|
|
247
|
+
className="self-start px-3 py-1.5 rounded-[8px] border border-dashed border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:border-white/[0.15] hover:text-text-2"
|
|
248
|
+
style={{ fontFamily: 'inherit' }}
|
|
249
|
+
>
|
|
250
|
+
+ Add Cron Job
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{!schedules.length && !gatewayCrons.length && !cronLoading && !showCronForm && (
|
|
257
|
+
<div className="text-[13px] text-text-3/50">No automations linked to this agent.</div>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function AdvancedTab({ agent }: { agent: Agent }) {
|
|
264
|
+
const isOpenClaw = agent.provider === 'openclaw'
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="p-4 flex flex-col gap-4">
|
|
268
|
+
{/* Permission Presets + Exec Config + Sandbox Env (OpenClaw only) */}
|
|
269
|
+
{isOpenClaw && (
|
|
270
|
+
<>
|
|
271
|
+
<PermissionPresetSelector agentId={agent.id} />
|
|
272
|
+
<div className="border-t border-white/[0.06] pt-4">
|
|
273
|
+
<ExecConfigPanel agentId={agent.id} />
|
|
274
|
+
</div>
|
|
275
|
+
<div className="border-t border-white/[0.06] pt-4">
|
|
276
|
+
<SandboxEnvPanel />
|
|
277
|
+
</div>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{agent.heartbeatEnabled && (
|
|
282
|
+
<div>
|
|
283
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Heartbeat</label>
|
|
284
|
+
<p className="text-[13px] text-text-2">
|
|
285
|
+
Every {agent.heartbeatIntervalSec ?? 120}s
|
|
286
|
+
{agent.heartbeatModel && ` (${agent.heartbeatModel})`}
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
{agent.thinkingLevel && (
|
|
291
|
+
<div>
|
|
292
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Thinking Level</label>
|
|
293
|
+
<p className="text-[13px] text-text-2 capitalize">{agent.thinkingLevel}</p>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
<div>
|
|
297
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Agent ID</label>
|
|
298
|
+
<p className="text-[12px] text-text-3 font-mono select-all">{agent.id}</p>
|
|
299
|
+
</div>
|
|
300
|
+
<div>
|
|
301
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Created</label>
|
|
302
|
+
<p className="text-[12px] text-text-3">{new Date(agent.createdAt).toLocaleString()}</p>
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Updated</label>
|
|
306
|
+
<p className="text-[12px] text-text-3">{new Date(agent.updatedAt).toLocaleString()}</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import { SkillInstallDialog } from './skill-install-dialog'
|
|
7
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
agentId: string
|
|
11
|
+
initialMode?: SkillAllowlistMode
|
|
12
|
+
initialAllowed?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SOURCE_ORDER: OpenClawSkillEntry['source'][] = ['bundled', 'managed', 'personal', 'workspace']
|
|
16
|
+
|
|
17
|
+
export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllowed = [] }: Props) {
|
|
18
|
+
const [skills, setSkills] = useState<OpenClawSkillEntry[]>([])
|
|
19
|
+
const [loading, setLoading] = useState(true)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const [mode, setMode] = useState<SkillAllowlistMode>(initialMode)
|
|
22
|
+
const [allowed, setAllowed] = useState<Set<string>>(new Set(initialAllowed))
|
|
23
|
+
const [saving, setSaving] = useState(false)
|
|
24
|
+
const [installTarget, setInstallTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
25
|
+
const [removeTarget, setRemoveTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
26
|
+
|
|
27
|
+
const loadSkills = useCallback(async () => {
|
|
28
|
+
setLoading(true)
|
|
29
|
+
setError(null)
|
|
30
|
+
try {
|
|
31
|
+
const result = await api<OpenClawSkillEntry[]>('GET', `/openclaw/skills?agentId=${agentId}`)
|
|
32
|
+
setSkills(Array.isArray(result) ? result : [])
|
|
33
|
+
} catch (err: unknown) {
|
|
34
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}, [agentId])
|
|
39
|
+
|
|
40
|
+
useEffect(() => { loadSkills() }, [loadSkills])
|
|
41
|
+
|
|
42
|
+
const handleModeChange = (newMode: SkillAllowlistMode) => {
|
|
43
|
+
setMode(newMode)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toggleSkill = (name: string) => {
|
|
47
|
+
setAllowed((prev) => {
|
|
48
|
+
const next = new Set(prev)
|
|
49
|
+
if (next.has(name)) next.delete(name)
|
|
50
|
+
else next.add(name)
|
|
51
|
+
return next
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleSave = async () => {
|
|
56
|
+
setSaving(true)
|
|
57
|
+
try {
|
|
58
|
+
await api('PUT', '/openclaw/skills', {
|
|
59
|
+
agentId,
|
|
60
|
+
mode,
|
|
61
|
+
allowedSkills: Array.from(allowed),
|
|
62
|
+
})
|
|
63
|
+
} catch {
|
|
64
|
+
// toast or ignore
|
|
65
|
+
} finally {
|
|
66
|
+
setSaving(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const grouped = SOURCE_ORDER
|
|
71
|
+
.map((source) => ({
|
|
72
|
+
source,
|
|
73
|
+
items: skills.filter((s) => s.source === source),
|
|
74
|
+
}))
|
|
75
|
+
.filter((g) => g.items.length > 0)
|
|
76
|
+
|
|
77
|
+
if (loading) {
|
|
78
|
+
return <div className="flex items-center justify-center h-32 text-[13px] text-text-3/50">Loading skills...</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return <div className="flex items-center justify-center h-32 text-[13px] text-red-400">{error}</div>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex flex-col gap-4 p-2">
|
|
87
|
+
{/* Mode selector */}
|
|
88
|
+
<div className="flex gap-1">
|
|
89
|
+
{(['all', 'none', 'selected'] as const).map((m) => (
|
|
90
|
+
<button
|
|
91
|
+
key={m}
|
|
92
|
+
onClick={() => handleModeChange(m)}
|
|
93
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
|
|
94
|
+
${mode === m ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
95
|
+
style={{ fontFamily: 'inherit' }}
|
|
96
|
+
>
|
|
97
|
+
{m === 'selected' ? 'Custom' : m}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Skill groups */}
|
|
103
|
+
{grouped.map(({ source, items }) => (
|
|
104
|
+
<div key={source}>
|
|
105
|
+
<h4 className="text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2 px-1">
|
|
106
|
+
{source}
|
|
107
|
+
</h4>
|
|
108
|
+
<div className="flex flex-col gap-1">
|
|
109
|
+
{items.map((skill) => (
|
|
110
|
+
<div
|
|
111
|
+
key={skill.name}
|
|
112
|
+
className="flex items-center gap-3 py-2 px-3 rounded-[10px] bg-white/[0.02] border border-white/[0.04]"
|
|
113
|
+
>
|
|
114
|
+
{mode === 'selected' && (
|
|
115
|
+
<button
|
|
116
|
+
onClick={() => toggleSkill(skill.name)}
|
|
117
|
+
className={`w-5 h-5 rounded-[5px] border-2 flex items-center justify-center shrink-0 cursor-pointer transition-all
|
|
118
|
+
${allowed.has(skill.name)
|
|
119
|
+
? 'bg-accent-bright border-accent-bright'
|
|
120
|
+
: 'bg-transparent border-white/[0.15] hover:border-white/[0.25]'}`}
|
|
121
|
+
>
|
|
122
|
+
{allowed.has(skill.name) && (
|
|
123
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round">
|
|
124
|
+
<polyline points="20 6 9 17 4 12" />
|
|
125
|
+
</svg>
|
|
126
|
+
)}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
<div className="flex-1 min-w-0">
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<span className="text-[13px] font-600 text-text truncate">{skill.name}</span>
|
|
132
|
+
<span className={`shrink-0 text-[9px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px]
|
|
133
|
+
${skill.eligible
|
|
134
|
+
? 'text-emerald-400 bg-emerald-400/[0.08]'
|
|
135
|
+
: skill.missing?.length
|
|
136
|
+
? 'text-amber-400 bg-amber-400/[0.08]'
|
|
137
|
+
: 'text-red-400 bg-red-400/[0.08]'}`}>
|
|
138
|
+
{skill.eligible ? 'ready' : 'missing deps'}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
{skill.description && (
|
|
142
|
+
<p className="text-[11px] text-text-3/60 mt-0.5 truncate">{skill.description}</p>
|
|
143
|
+
)}
|
|
144
|
+
{skill.missing && skill.missing.length > 0 && (
|
|
145
|
+
<p className="text-[10px] text-amber-400/60 mt-0.5">
|
|
146
|
+
Needs: {skill.missing.join(', ')}
|
|
147
|
+
</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
{/* Action buttons */}
|
|
151
|
+
<div className="flex gap-1 shrink-0">
|
|
152
|
+
{!skill.eligible && skill.installOptions && skill.installOptions.length > 0 && (
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setInstallTarget(skill)}
|
|
155
|
+
className="text-[10px] text-accent-bright bg-transparent border-none cursor-pointer hover:underline"
|
|
156
|
+
>
|
|
157
|
+
Install
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
{skill.skillKey && (
|
|
161
|
+
<button
|
|
162
|
+
onClick={async () => {
|
|
163
|
+
await api('PATCH', '/openclaw/skills', { skillKey: skill.skillKey, enabled: !skill.disabled })
|
|
164
|
+
loadSkills()
|
|
165
|
+
}}
|
|
166
|
+
className={`text-[10px] bg-transparent border-none cursor-pointer hover:underline ${skill.disabled ? 'text-emerald-400' : 'text-amber-400'}`}
|
|
167
|
+
>
|
|
168
|
+
{skill.disabled ? 'Enable' : 'Disable'}
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
{skill.skillKey && skill.source !== 'bundled' && (
|
|
172
|
+
<button
|
|
173
|
+
onClick={() => setRemoveTarget(skill)}
|
|
174
|
+
className="text-[10px] text-red-400/70 bg-transparent border-none cursor-pointer hover:underline"
|
|
175
|
+
>
|
|
176
|
+
Remove
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
|
|
186
|
+
{skills.length === 0 && (
|
|
187
|
+
<div className="text-[13px] text-text-3/50 text-center py-4">No skills discovered</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Save button */}
|
|
191
|
+
<button
|
|
192
|
+
onClick={handleSave}
|
|
193
|
+
disabled={saving}
|
|
194
|
+
className="px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600
|
|
195
|
+
cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed transition-all hover:brightness-110 self-start"
|
|
196
|
+
style={{ fontFamily: 'inherit' }}
|
|
197
|
+
>
|
|
198
|
+
{saving ? 'Saving...' : 'Save Configuration'}
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
{/* Install dialog */}
|
|
202
|
+
{installTarget && (
|
|
203
|
+
<SkillInstallDialog
|
|
204
|
+
open={!!installTarget}
|
|
205
|
+
onClose={() => setInstallTarget(null)}
|
|
206
|
+
skillName={installTarget.name}
|
|
207
|
+
installOptions={installTarget.installOptions}
|
|
208
|
+
onInstalled={loadSkills}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Remove confirm */}
|
|
213
|
+
{removeTarget && (
|
|
214
|
+
<ConfirmDialog
|
|
215
|
+
open={!!removeTarget}
|
|
216
|
+
title="Remove Skill"
|
|
217
|
+
message={`Remove "${removeTarget.name}"? This cannot be undone.`}
|
|
218
|
+
confirmLabel="Remove"
|
|
219
|
+
danger
|
|
220
|
+
onConfirm={async () => {
|
|
221
|
+
await api('POST', '/openclaw/skills/remove', { skillKey: removeTarget.skillKey, source: removeTarget.source })
|
|
222
|
+
setRemoveTarget(null)
|
|
223
|
+
loadSkills()
|
|
224
|
+
}}
|
|
225
|
+
onCancel={() => setRemoveTarget(null)}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { PermissionPreset } from '@/types'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
agentId: string
|
|
9
|
+
onPresetChanged?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PRESETS: { id: PermissionPreset; label: string; desc: string; color: string }[] = [
|
|
13
|
+
{ id: 'conservative', label: 'Conservative', desc: 'Block all exec, no tools', color: 'text-red-400 bg-red-400/[0.08] border-red-400/20' },
|
|
14
|
+
{ id: 'collaborative', label: 'Collaborative', desc: 'Allowlist exec, web + fs tools', color: 'text-amber-300 bg-amber-400/[0.08] border-amber-400/20' },
|
|
15
|
+
{ id: 'autonomous', label: 'Autonomous', desc: 'Full exec, all tools', color: 'text-emerald-400 bg-emerald-400/[0.08] border-emerald-400/20' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function PermissionPresetSelector({ agentId, onPresetChanged }: Props) {
|
|
19
|
+
const [current, setCurrent] = useState<PermissionPreset | 'custom' | null>(null)
|
|
20
|
+
const [loading, setLoading] = useState(true)
|
|
21
|
+
const [applying, setApplying] = useState(false)
|
|
22
|
+
|
|
23
|
+
const load = useCallback(async () => {
|
|
24
|
+
setLoading(true)
|
|
25
|
+
try {
|
|
26
|
+
const res = await api<{ preset: PermissionPreset | 'custom' }>('GET', `/openclaw/permissions?agentId=${agentId}`)
|
|
27
|
+
setCurrent(res.preset)
|
|
28
|
+
} catch {
|
|
29
|
+
setCurrent(null)
|
|
30
|
+
} finally {
|
|
31
|
+
setLoading(false)
|
|
32
|
+
}
|
|
33
|
+
}, [agentId])
|
|
34
|
+
|
|
35
|
+
useEffect(() => { load() }, [load])
|
|
36
|
+
|
|
37
|
+
const handleSelect = async (preset: PermissionPreset) => {
|
|
38
|
+
if (applying || preset === current) return
|
|
39
|
+
setApplying(true)
|
|
40
|
+
try {
|
|
41
|
+
await api('PUT', '/openclaw/permissions', { agentId, preset })
|
|
42
|
+
setCurrent(preset)
|
|
43
|
+
onPresetChanged?.()
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
} finally {
|
|
47
|
+
setApplying(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (loading) return <div className="text-[12px] text-text-3/50 py-2">Loading presets...</div>
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-2">
|
|
55
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">Permission Preset</label>
|
|
56
|
+
<div className="flex gap-2">
|
|
57
|
+
{PRESETS.map((p) => (
|
|
58
|
+
<button
|
|
59
|
+
key={p.id}
|
|
60
|
+
onClick={() => handleSelect(p.id)}
|
|
61
|
+
disabled={applying}
|
|
62
|
+
className={`flex-1 flex flex-col items-center gap-1 py-2.5 px-2 rounded-[10px] border cursor-pointer transition-all
|
|
63
|
+
${current === p.id
|
|
64
|
+
? p.color
|
|
65
|
+
: 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:border-white/[0.12]'
|
|
66
|
+
} ${applying ? 'opacity-50' : ''}`}
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
<span className="text-[11px] font-600">{p.label}</span>
|
|
70
|
+
<span className="text-[9px] text-text-3/50 text-center leading-tight">{p.desc}</span>
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
{current === 'custom' && (
|
|
75
|
+
<span className="text-[10px] text-text-3/50">Custom configuration — select a preset to override</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|