@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.
Files changed (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. 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
+ }