@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
|
@@ -4,6 +4,8 @@ import { useEffect, useMemo, useState, useCallback } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { AgentCard } from './agent-card'
|
|
7
|
+
import { TrashList } from './trash-list'
|
|
8
|
+
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
inSidebar?: boolean
|
|
@@ -17,6 +19,11 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
17
19
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
18
20
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
19
21
|
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
22
|
+
const showTrash = useAppStore((s) => s.showTrash)
|
|
23
|
+
const setShowTrash = useAppStore((s) => s.setShowTrash)
|
|
24
|
+
const fleetFilter = useAppStore((s) => s.fleetFilter)
|
|
25
|
+
const setFleetFilter = useAppStore((s) => s.setFleetFilter)
|
|
26
|
+
const approvals = useApprovalStore((s) => s.approvals)
|
|
20
27
|
const [search, setSearch] = useState('')
|
|
21
28
|
const [filter, setFilter] = useState<'all' | 'orchestrator' | 'agent'>('all')
|
|
22
29
|
|
|
@@ -36,6 +43,24 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
36
43
|
|
|
37
44
|
useEffect(() => { loadAgents() }, [])
|
|
38
45
|
|
|
46
|
+
// Compute which agents are "running" (have active sessions)
|
|
47
|
+
const runningAgentIds = useMemo(() => {
|
|
48
|
+
const ids = new Set<string>()
|
|
49
|
+
for (const s of Object.values(sessions)) {
|
|
50
|
+
if (s.agentId && s.active) ids.add(s.agentId)
|
|
51
|
+
}
|
|
52
|
+
return ids
|
|
53
|
+
}, [sessions])
|
|
54
|
+
|
|
55
|
+
// Approval counts per agent
|
|
56
|
+
const approvalsByAgent = useMemo(() => {
|
|
57
|
+
const counts: Record<string, number> = {}
|
|
58
|
+
for (const a of Object.values(approvals)) {
|
|
59
|
+
counts[a.agentId] = (counts[a.agentId] || 0) + 1
|
|
60
|
+
}
|
|
61
|
+
return counts
|
|
62
|
+
}, [approvals])
|
|
63
|
+
|
|
39
64
|
const filtered = useMemo(() => {
|
|
40
65
|
return Object.values(agents)
|
|
41
66
|
.filter((p) => {
|
|
@@ -43,10 +68,34 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
43
68
|
if (filter === 'orchestrator' && !p.isOrchestrator) return false
|
|
44
69
|
if (filter === 'agent' && p.isOrchestrator) return false
|
|
45
70
|
if (activeProjectFilter && p.projectId !== activeProjectFilter) return false
|
|
71
|
+
// Fleet filter
|
|
72
|
+
if (fleetFilter === 'running' && !runningAgentIds.has(p.id)) return false
|
|
73
|
+
if (fleetFilter === 'approvals' && !(approvalsByAgent[p.id] > 0)) return false
|
|
46
74
|
return true
|
|
47
75
|
})
|
|
48
76
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
49
|
-
}, [agents, search, filter, activeProjectFilter])
|
|
77
|
+
}, [agents, search, filter, activeProjectFilter, fleetFilter, runningAgentIds, approvalsByAgent])
|
|
78
|
+
|
|
79
|
+
if (showTrash) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
82
|
+
<div className="px-4 py-2.5 flex items-center gap-2">
|
|
83
|
+
<button
|
|
84
|
+
onClick={() => setShowTrash(false)}
|
|
85
|
+
className="px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-text-3 bg-transparent border-none cursor-pointer hover:text-text-2 transition-all flex items-center gap-1.5"
|
|
86
|
+
style={{ fontFamily: 'inherit' }}
|
|
87
|
+
>
|
|
88
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
89
|
+
<path d="M19 12H5" /><polyline points="12 19 5 12 12 5" />
|
|
90
|
+
</svg>
|
|
91
|
+
Back to Agents
|
|
92
|
+
</button>
|
|
93
|
+
<span className="text-[13px] font-600 text-text-2">Trash</span>
|
|
94
|
+
</div>
|
|
95
|
+
<TrashList />
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
50
99
|
|
|
51
100
|
if (!filtered.length && !search) {
|
|
52
101
|
return (
|
|
@@ -89,7 +138,26 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
89
138
|
/>
|
|
90
139
|
</div>
|
|
91
140
|
)}
|
|
92
|
-
|
|
141
|
+
{/* Fleet filter: All / Running / Approvals */}
|
|
142
|
+
<div className="flex gap-1 px-4 pb-1 items-center">
|
|
143
|
+
{(['all', 'running', 'approvals'] as const).map((f) => {
|
|
144
|
+
const count = f === 'running' ? runningAgentIds.size
|
|
145
|
+
: f === 'approvals' ? Object.keys(approvalsByAgent).length
|
|
146
|
+
: null
|
|
147
|
+
return (
|
|
148
|
+
<button
|
|
149
|
+
key={f}
|
|
150
|
+
onClick={() => setFleetFilter(f)}
|
|
151
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
|
|
152
|
+
${fleetFilter === f ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
153
|
+
style={{ fontFamily: 'inherit' }}
|
|
154
|
+
>
|
|
155
|
+
{f}{count ? ` (${count})` : ''}
|
|
156
|
+
</button>
|
|
157
|
+
)
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex gap-1 px-4 pb-2 items-center">
|
|
93
161
|
{(['all', 'orchestrator', 'agent'] as const).map((f) => (
|
|
94
162
|
<button
|
|
95
163
|
key={f}
|
|
@@ -101,10 +169,21 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
101
169
|
{f}
|
|
102
170
|
</button>
|
|
103
171
|
))}
|
|
172
|
+
<div className="flex-1" />
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => setShowTrash(true)}
|
|
175
|
+
aria-label="View trash"
|
|
176
|
+
className="p-1.5 rounded-[6px] text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer transition-all hover:bg-white/[0.04]"
|
|
177
|
+
>
|
|
178
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
179
|
+
<polyline points="3 6 5 6 21 6" />
|
|
180
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
104
183
|
</div>
|
|
105
184
|
<div className="flex flex-col gap-1 px-2 pb-4">
|
|
106
185
|
{filtered.map((p) => (
|
|
107
|
-
<AgentCard key={p.id} agent={p} isDefault={p.id === defaultAgentId} onSetDefault={handleSetDefault} />
|
|
186
|
+
<AgentCard key={p.id} agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} onSetDefault={handleSetDefault} />
|
|
108
187
|
))}
|
|
109
188
|
</div>
|
|
110
189
|
</div>
|
|
@@ -10,6 +10,7 @@ import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
|
10
10
|
import type { ProviderType, ClaudeSkill } from '@/types'
|
|
11
11
|
import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
|
|
12
12
|
import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
13
|
+
import { AgentAvatar } from './agent-avatar'
|
|
13
14
|
|
|
14
15
|
const HB_PRESETS = [30, 60, 120, 300, 600, 1800, 3600] as const
|
|
15
16
|
|
|
@@ -97,6 +98,7 @@ export function AgentSheet() {
|
|
|
97
98
|
const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
|
|
98
99
|
const [openclawEnabled, setOpenclawEnabled] = useState(false)
|
|
99
100
|
const [projectId, setProjectId] = useState<string | undefined>(undefined)
|
|
101
|
+
const [avatarSeed, setAvatarSeed] = useState('')
|
|
100
102
|
const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
|
|
101
103
|
const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
|
|
102
104
|
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
|
|
@@ -171,6 +173,7 @@ export function AgentSheet() {
|
|
|
171
173
|
setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
|
|
172
174
|
setOpenclawEnabled(editing.provider === 'openclaw')
|
|
173
175
|
setProjectId(editing.projectId)
|
|
176
|
+
setAvatarSeed(editing.avatarSeed || '')
|
|
174
177
|
setThinkingLevel(editing.thinkingLevel || '')
|
|
175
178
|
setHeartbeatEnabled(editing.heartbeatEnabled || false)
|
|
176
179
|
setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
|
|
@@ -198,6 +201,7 @@ export function AgentSheet() {
|
|
|
198
201
|
setOllamaMode('local')
|
|
199
202
|
setOpenclawEnabled(false)
|
|
200
203
|
setProjectId(undefined)
|
|
204
|
+
setAvatarSeed('')
|
|
201
205
|
setThinkingLevel('')
|
|
202
206
|
setHeartbeatEnabled(false)
|
|
203
207
|
setHeartbeatIntervalSec('')
|
|
@@ -291,6 +295,7 @@ export function AgentSheet() {
|
|
|
291
295
|
platformAssignScope,
|
|
292
296
|
capabilities,
|
|
293
297
|
projectId: projectId || undefined,
|
|
298
|
+
avatarSeed: avatarSeed.trim() || undefined,
|
|
294
299
|
thinkingLevel: thinkingLevel || undefined,
|
|
295
300
|
heartbeatEnabled,
|
|
296
301
|
heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
|
|
@@ -451,6 +456,29 @@ export function AgentSheet() {
|
|
|
451
456
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
452
457
|
</div>
|
|
453
458
|
|
|
459
|
+
<div className="mb-8">
|
|
460
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Avatar</label>
|
|
461
|
+
<div className="flex items-center gap-3">
|
|
462
|
+
<AgentAvatar seed={avatarSeed || null} name={name || 'A'} size={40} />
|
|
463
|
+
<input
|
|
464
|
+
type="text"
|
|
465
|
+
value={avatarSeed}
|
|
466
|
+
onChange={(e) => setAvatarSeed(e.target.value)}
|
|
467
|
+
placeholder="Avatar seed (any text)"
|
|
468
|
+
className={inputClass}
|
|
469
|
+
style={{ fontFamily: 'inherit', flex: 1 }}
|
|
470
|
+
/>
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
|
|
474
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
|
|
475
|
+
style={{ fontFamily: 'inherit' }}
|
|
476
|
+
>
|
|
477
|
+
Randomize
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
454
482
|
<div className="mb-8">
|
|
455
483
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
|
|
456
484
|
<input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
@@ -867,6 +895,8 @@ export function AgentSheet() {
|
|
|
867
895
|
</div>
|
|
868
896
|
)}
|
|
869
897
|
|
|
898
|
+
{/* OpenClaw manages its own models — no selector needed */}
|
|
899
|
+
|
|
870
900
|
{/* Ollama Mode Toggle */}
|
|
871
901
|
{!openclawEnabled && provider === 'ollama' && (
|
|
872
902
|
<div className="mb-8">
|
|
@@ -1351,3 +1381,4 @@ export function AgentSheet() {
|
|
|
1351
1381
|
</BottomSheet>
|
|
1352
1382
|
)
|
|
1353
1383
|
}
|
|
1384
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { GatewayCronJob } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
agentId: string
|
|
9
|
+
onSaved: () => void
|
|
10
|
+
onCancel: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CronJobForm({ agentId, onSaved, onCancel }: Props) {
|
|
14
|
+
const [name, setName] = useState('')
|
|
15
|
+
const [scheduleKind, setScheduleKind] = useState<'at' | 'every' | 'cron'>('every')
|
|
16
|
+
const [scheduleValue, setScheduleValue] = useState('1h')
|
|
17
|
+
const [timezone, setTimezone] = useState('')
|
|
18
|
+
const [payloadKind, setPayloadKind] = useState<'systemEvent' | 'agentTurn'>('agentTurn')
|
|
19
|
+
const [payloadText, setPayloadText] = useState('')
|
|
20
|
+
const [sessionTarget, setSessionTarget] = useState<'main' | 'isolated'>('main')
|
|
21
|
+
const [saving, setSaving] = useState(false)
|
|
22
|
+
const [error, setError] = useState('')
|
|
23
|
+
|
|
24
|
+
const handleSave = async () => {
|
|
25
|
+
if (!name.trim()) return
|
|
26
|
+
setSaving(true)
|
|
27
|
+
setError('')
|
|
28
|
+
|
|
29
|
+
const job: Partial<GatewayCronJob> = {
|
|
30
|
+
name: name.trim(),
|
|
31
|
+
agentId,
|
|
32
|
+
enabled: true,
|
|
33
|
+
schedule: { kind: scheduleKind, value: scheduleValue, timezone: timezone || undefined },
|
|
34
|
+
payload: {
|
|
35
|
+
kind: payloadKind,
|
|
36
|
+
...(payloadKind === 'agentTurn' ? { message: payloadText } : { text: payloadText }),
|
|
37
|
+
},
|
|
38
|
+
sessionTarget,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await api('POST', '/openclaw/cron', { action: 'add', job })
|
|
43
|
+
onSaved()
|
|
44
|
+
} catch (err: unknown) {
|
|
45
|
+
setError(err instanceof Error ? err.message : 'Failed to create')
|
|
46
|
+
} finally {
|
|
47
|
+
setSaving(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const inputClass = 'w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors'
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-3 p-4 border border-white/[0.06] rounded-[12px] bg-white/[0.02]">
|
|
55
|
+
<div>
|
|
56
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Name</label>
|
|
57
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Job name" className={inputClass} />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="grid grid-cols-2 gap-2">
|
|
61
|
+
<div>
|
|
62
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Schedule Type</label>
|
|
63
|
+
<select value={scheduleKind} onChange={(e) => setScheduleKind(e.target.value as typeof scheduleKind)} className={inputClass}>
|
|
64
|
+
<option value="every">Every (interval)</option>
|
|
65
|
+
<option value="at">At (specific time)</option>
|
|
66
|
+
<option value="cron">Cron expression</option>
|
|
67
|
+
</select>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Value</label>
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
value={scheduleValue}
|
|
74
|
+
onChange={(e) => setScheduleValue(e.target.value)}
|
|
75
|
+
placeholder={scheduleKind === 'cron' ? '0 */6 * * *' : scheduleKind === 'at' ? '09:00' : '1h'}
|
|
76
|
+
className={inputClass}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{scheduleKind !== 'every' && (
|
|
82
|
+
<div>
|
|
83
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Timezone</label>
|
|
84
|
+
<input type="text" value={timezone} onChange={(e) => setTimezone(e.target.value)} placeholder="America/New_York" className={inputClass} />
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<div className="grid grid-cols-2 gap-2">
|
|
89
|
+
<div>
|
|
90
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Payload</label>
|
|
91
|
+
<select value={payloadKind} onChange={(e) => setPayloadKind(e.target.value as typeof payloadKind)} className={inputClass}>
|
|
92
|
+
<option value="agentTurn">Agent Turn</option>
|
|
93
|
+
<option value="systemEvent">System Event</option>
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Session</label>
|
|
98
|
+
<select value={sessionTarget} onChange={(e) => setSessionTarget(e.target.value as typeof sessionTarget)} className={inputClass}>
|
|
99
|
+
<option value="main">Main session</option>
|
|
100
|
+
<option value="isolated">Isolated session</option>
|
|
101
|
+
</select>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div>
|
|
106
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1">Message / Text</label>
|
|
107
|
+
<textarea
|
|
108
|
+
value={payloadText}
|
|
109
|
+
onChange={(e) => setPayloadText(e.target.value)}
|
|
110
|
+
placeholder="Message to send..."
|
|
111
|
+
rows={2}
|
|
112
|
+
className={`${inputClass} resize-none`}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
117
|
+
|
|
118
|
+
<div className="flex gap-2 justify-end">
|
|
119
|
+
<button
|
|
120
|
+
onClick={onCancel}
|
|
121
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04]"
|
|
122
|
+
style={{ fontFamily: 'inherit' }}
|
|
123
|
+
>
|
|
124
|
+
Cancel
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
onClick={handleSave}
|
|
128
|
+
disabled={saving || !name.trim()}
|
|
129
|
+
className="px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
130
|
+
style={{ fontFamily: 'inherit' }}
|
|
131
|
+
>
|
|
132
|
+
{saving ? 'Creating...' : 'Create'}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { ExecApprovalConfig, ExecApprovalSnapshot } from '@/types'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
agentId: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ExecConfigPanel({ agentId }: Props) {
|
|
12
|
+
const [config, setConfig] = useState<ExecApprovalConfig>({ security: 'deny', askMode: 'off', patterns: [] })
|
|
13
|
+
const [hash, setHash] = useState('')
|
|
14
|
+
const [loading, setLoading] = useState(true)
|
|
15
|
+
const [saving, setSaving] = useState(false)
|
|
16
|
+
const [error, setError] = useState('')
|
|
17
|
+
const [newPattern, setNewPattern] = useState('')
|
|
18
|
+
|
|
19
|
+
const load = useCallback(async () => {
|
|
20
|
+
setLoading(true)
|
|
21
|
+
setError('')
|
|
22
|
+
try {
|
|
23
|
+
const snap = await api<ExecApprovalSnapshot>('GET', `/openclaw/exec-config?agentId=${agentId}`)
|
|
24
|
+
setConfig(snap.file)
|
|
25
|
+
setHash(snap.hash)
|
|
26
|
+
} catch (err: unknown) {
|
|
27
|
+
setError(err instanceof Error ? err.message : 'Failed to load')
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}, [agentId])
|
|
32
|
+
|
|
33
|
+
useEffect(() => { load() }, [load])
|
|
34
|
+
|
|
35
|
+
const save = async (patch: Partial<ExecApprovalConfig>) => {
|
|
36
|
+
const updated = { ...config, ...patch }
|
|
37
|
+
setConfig(updated)
|
|
38
|
+
setSaving(true)
|
|
39
|
+
setError('')
|
|
40
|
+
try {
|
|
41
|
+
const result = await api<{ ok: boolean; hash: string }>('PUT', '/openclaw/exec-config', {
|
|
42
|
+
agentId,
|
|
43
|
+
config: updated,
|
|
44
|
+
baseHash: hash,
|
|
45
|
+
})
|
|
46
|
+
setHash(result.hash)
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
setError(err instanceof Error ? err.message : 'Save failed')
|
|
49
|
+
} finally {
|
|
50
|
+
setSaving(false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const addPattern = () => {
|
|
55
|
+
const p = newPattern.trim()
|
|
56
|
+
if (!p || config.patterns.includes(p)) return
|
|
57
|
+
save({ patterns: [...config.patterns, p] })
|
|
58
|
+
setNewPattern('')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const removePattern = (idx: number) => {
|
|
62
|
+
save({ patterns: config.patterns.filter((_, i) => i !== idx) })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (loading) return <div className="p-4 text-[13px] text-text-3/50">Loading exec config...</div>
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col gap-4">
|
|
69
|
+
{/* Security Level */}
|
|
70
|
+
<div>
|
|
71
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2">Security Level</label>
|
|
72
|
+
<select
|
|
73
|
+
value={config.security}
|
|
74
|
+
onChange={(e) => save({ security: e.target.value as ExecApprovalConfig['security'] })}
|
|
75
|
+
disabled={saving}
|
|
76
|
+
className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none"
|
|
77
|
+
>
|
|
78
|
+
<option value="deny">Deny (block all)</option>
|
|
79
|
+
<option value="allowlist">Allowlist (matched patterns only)</option>
|
|
80
|
+
<option value="full">Full (allow all)</option>
|
|
81
|
+
</select>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Ask Mode */}
|
|
85
|
+
<div>
|
|
86
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2">Ask Mode</label>
|
|
87
|
+
<select
|
|
88
|
+
value={config.askMode}
|
|
89
|
+
onChange={(e) => save({ askMode: e.target.value as ExecApprovalConfig['askMode'] })}
|
|
90
|
+
disabled={saving}
|
|
91
|
+
className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none"
|
|
92
|
+
>
|
|
93
|
+
<option value="off">Off</option>
|
|
94
|
+
<option value="on-miss">On miss (ask when no pattern matches)</option>
|
|
95
|
+
<option value="always">Always ask</option>
|
|
96
|
+
</select>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Patterns */}
|
|
100
|
+
{config.security === 'allowlist' && (
|
|
101
|
+
<div>
|
|
102
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2">
|
|
103
|
+
Allowed Patterns
|
|
104
|
+
</label>
|
|
105
|
+
<div className="flex flex-col gap-1 mb-2">
|
|
106
|
+
{config.patterns.map((p, i) => (
|
|
107
|
+
<div key={i} className="flex items-center gap-2 py-1 px-2 rounded-[8px] bg-white/[0.02] border border-white/[0.04]">
|
|
108
|
+
<span className="text-[12px] text-text font-mono truncate flex-1">{p}</span>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => removePattern(i)}
|
|
111
|
+
disabled={saving}
|
|
112
|
+
className="text-red-400/60 hover:text-red-400 text-[10px] bg-transparent border-none cursor-pointer"
|
|
113
|
+
>
|
|
114
|
+
Remove
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
{!config.patterns.length && (
|
|
119
|
+
<span className="text-[12px] text-text-3/40">No patterns configured</span>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex gap-2">
|
|
123
|
+
<input
|
|
124
|
+
type="text"
|
|
125
|
+
value={newPattern}
|
|
126
|
+
onChange={(e) => setNewPattern(e.target.value)}
|
|
127
|
+
onKeyDown={(e) => e.key === 'Enter' && addPattern()}
|
|
128
|
+
placeholder="e.g. npm run *"
|
|
129
|
+
className="flex-1 px-3 py-1.5 rounded-[8px] border border-white/[0.06] bg-black/20 text-[12px] text-text font-mono outline-none placeholder:text-text-3/40"
|
|
130
|
+
/>
|
|
131
|
+
<button
|
|
132
|
+
onClick={addPattern}
|
|
133
|
+
disabled={saving || !newPattern.trim()}
|
|
134
|
+
className="px-3 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[11px] font-600 cursor-pointer disabled:opacity-30 transition-all"
|
|
135
|
+
style={{ fontFamily: 'inherit' }}
|
|
136
|
+
>
|
|
137
|
+
Add
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
144
|
+
{saving && <p className="text-[11px] text-text-3/50">Saving...</p>}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|