@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.
Files changed (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. 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
- <div className="flex gap-1 px-4 pb-2">
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
+ }