@swarmclawai/swarmclaw 0.4.0 → 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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- 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 +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- 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 +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -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 +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -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 +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- 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 +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- 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 +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -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 +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- 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 +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|