@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,230 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import { SkillInstallDialog } from './skill-install-dialog'
|
|
7
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
agentId: string
|
|
11
|
+
initialMode?: SkillAllowlistMode
|
|
12
|
+
initialAllowed?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SOURCE_ORDER: OpenClawSkillEntry['source'][] = ['bundled', 'managed', 'personal', 'workspace']
|
|
16
|
+
|
|
17
|
+
export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllowed = [] }: Props) {
|
|
18
|
+
const [skills, setSkills] = useState<OpenClawSkillEntry[]>([])
|
|
19
|
+
const [loading, setLoading] = useState(true)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const [mode, setMode] = useState<SkillAllowlistMode>(initialMode)
|
|
22
|
+
const [allowed, setAllowed] = useState<Set<string>>(new Set(initialAllowed))
|
|
23
|
+
const [saving, setSaving] = useState(false)
|
|
24
|
+
const [installTarget, setInstallTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
25
|
+
const [removeTarget, setRemoveTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
26
|
+
|
|
27
|
+
const loadSkills = useCallback(async () => {
|
|
28
|
+
setLoading(true)
|
|
29
|
+
setError(null)
|
|
30
|
+
try {
|
|
31
|
+
const result = await api<OpenClawSkillEntry[]>('GET', `/openclaw/skills?agentId=${agentId}`)
|
|
32
|
+
setSkills(Array.isArray(result) ? result : [])
|
|
33
|
+
} catch (err: unknown) {
|
|
34
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}, [agentId])
|
|
39
|
+
|
|
40
|
+
useEffect(() => { loadSkills() }, [loadSkills])
|
|
41
|
+
|
|
42
|
+
const handleModeChange = (newMode: SkillAllowlistMode) => {
|
|
43
|
+
setMode(newMode)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toggleSkill = (name: string) => {
|
|
47
|
+
setAllowed((prev) => {
|
|
48
|
+
const next = new Set(prev)
|
|
49
|
+
if (next.has(name)) next.delete(name)
|
|
50
|
+
else next.add(name)
|
|
51
|
+
return next
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleSave = async () => {
|
|
56
|
+
setSaving(true)
|
|
57
|
+
try {
|
|
58
|
+
await api('PUT', '/openclaw/skills', {
|
|
59
|
+
agentId,
|
|
60
|
+
mode,
|
|
61
|
+
allowedSkills: Array.from(allowed),
|
|
62
|
+
})
|
|
63
|
+
} catch {
|
|
64
|
+
// toast or ignore
|
|
65
|
+
} finally {
|
|
66
|
+
setSaving(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const grouped = SOURCE_ORDER
|
|
71
|
+
.map((source) => ({
|
|
72
|
+
source,
|
|
73
|
+
items: skills.filter((s) => s.source === source),
|
|
74
|
+
}))
|
|
75
|
+
.filter((g) => g.items.length > 0)
|
|
76
|
+
|
|
77
|
+
if (loading) {
|
|
78
|
+
return <div className="flex items-center justify-center h-32 text-[13px] text-text-3/50">Loading skills...</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return <div className="flex items-center justify-center h-32 text-[13px] text-red-400">{error}</div>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex flex-col gap-4 p-2">
|
|
87
|
+
{/* Mode selector */}
|
|
88
|
+
<div className="flex gap-1">
|
|
89
|
+
{(['all', 'none', 'selected'] as const).map((m) => (
|
|
90
|
+
<button
|
|
91
|
+
key={m}
|
|
92
|
+
onClick={() => handleModeChange(m)}
|
|
93
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
|
|
94
|
+
${mode === m ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
95
|
+
style={{ fontFamily: 'inherit' }}
|
|
96
|
+
>
|
|
97
|
+
{m === 'selected' ? 'Custom' : m}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Skill groups */}
|
|
103
|
+
{grouped.map(({ source, items }) => (
|
|
104
|
+
<div key={source}>
|
|
105
|
+
<h4 className="text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2 px-1">
|
|
106
|
+
{source}
|
|
107
|
+
</h4>
|
|
108
|
+
<div className="flex flex-col gap-1">
|
|
109
|
+
{items.map((skill) => (
|
|
110
|
+
<div
|
|
111
|
+
key={skill.name}
|
|
112
|
+
className="flex items-center gap-3 py-2 px-3 rounded-[10px] bg-white/[0.02] border border-white/[0.04]"
|
|
113
|
+
>
|
|
114
|
+
{mode === 'selected' && (
|
|
115
|
+
<button
|
|
116
|
+
onClick={() => toggleSkill(skill.name)}
|
|
117
|
+
className={`w-5 h-5 rounded-[5px] border-2 flex items-center justify-center shrink-0 cursor-pointer transition-all
|
|
118
|
+
${allowed.has(skill.name)
|
|
119
|
+
? 'bg-accent-bright border-accent-bright'
|
|
120
|
+
: 'bg-transparent border-white/[0.15] hover:border-white/[0.25]'}`}
|
|
121
|
+
>
|
|
122
|
+
{allowed.has(skill.name) && (
|
|
123
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round">
|
|
124
|
+
<polyline points="20 6 9 17 4 12" />
|
|
125
|
+
</svg>
|
|
126
|
+
)}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
<div className="flex-1 min-w-0">
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<span className="text-[13px] font-600 text-text truncate">{skill.name}</span>
|
|
132
|
+
<span className={`shrink-0 text-[9px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px]
|
|
133
|
+
${skill.eligible
|
|
134
|
+
? 'text-emerald-400 bg-emerald-400/[0.08]'
|
|
135
|
+
: skill.missing?.length
|
|
136
|
+
? 'text-amber-400 bg-amber-400/[0.08]'
|
|
137
|
+
: 'text-red-400 bg-red-400/[0.08]'}`}>
|
|
138
|
+
{skill.eligible ? 'ready' : 'missing deps'}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
{skill.description && (
|
|
142
|
+
<p className="text-[11px] text-text-3/60 mt-0.5 truncate">{skill.description}</p>
|
|
143
|
+
)}
|
|
144
|
+
{skill.missing && skill.missing.length > 0 && (
|
|
145
|
+
<p className="text-[10px] text-amber-400/60 mt-0.5">
|
|
146
|
+
Needs: {skill.missing.join(', ')}
|
|
147
|
+
</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
{/* Action buttons */}
|
|
151
|
+
<div className="flex gap-1 shrink-0">
|
|
152
|
+
{!skill.eligible && skill.installOptions && skill.installOptions.length > 0 && (
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => setInstallTarget(skill)}
|
|
155
|
+
className="text-[10px] text-accent-bright bg-transparent border-none cursor-pointer hover:underline"
|
|
156
|
+
>
|
|
157
|
+
Install
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
{skill.skillKey && (
|
|
161
|
+
<button
|
|
162
|
+
onClick={async () => {
|
|
163
|
+
await api('PATCH', '/openclaw/skills', { skillKey: skill.skillKey, enabled: !skill.disabled })
|
|
164
|
+
loadSkills()
|
|
165
|
+
}}
|
|
166
|
+
className={`text-[10px] bg-transparent border-none cursor-pointer hover:underline ${skill.disabled ? 'text-emerald-400' : 'text-amber-400'}`}
|
|
167
|
+
>
|
|
168
|
+
{skill.disabled ? 'Enable' : 'Disable'}
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
{skill.skillKey && skill.source !== 'bundled' && (
|
|
172
|
+
<button
|
|
173
|
+
onClick={() => setRemoveTarget(skill)}
|
|
174
|
+
className="text-[10px] text-red-400/70 bg-transparent border-none cursor-pointer hover:underline"
|
|
175
|
+
>
|
|
176
|
+
Remove
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
|
|
186
|
+
{skills.length === 0 && (
|
|
187
|
+
<div className="text-[13px] text-text-3/50 text-center py-4">No skills discovered</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Save button */}
|
|
191
|
+
<button
|
|
192
|
+
onClick={handleSave}
|
|
193
|
+
disabled={saving}
|
|
194
|
+
className="px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600
|
|
195
|
+
cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed transition-all hover:brightness-110 self-start"
|
|
196
|
+
style={{ fontFamily: 'inherit' }}
|
|
197
|
+
>
|
|
198
|
+
{saving ? 'Saving...' : 'Save Configuration'}
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
{/* Install dialog */}
|
|
202
|
+
{installTarget && (
|
|
203
|
+
<SkillInstallDialog
|
|
204
|
+
open={!!installTarget}
|
|
205
|
+
onClose={() => setInstallTarget(null)}
|
|
206
|
+
skillName={installTarget.name}
|
|
207
|
+
installOptions={installTarget.installOptions}
|
|
208
|
+
onInstalled={loadSkills}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Remove confirm */}
|
|
213
|
+
{removeTarget && (
|
|
214
|
+
<ConfirmDialog
|
|
215
|
+
open={!!removeTarget}
|
|
216
|
+
title="Remove Skill"
|
|
217
|
+
message={`Remove "${removeTarget.name}"? This cannot be undone.`}
|
|
218
|
+
confirmLabel="Remove"
|
|
219
|
+
danger
|
|
220
|
+
onConfirm={async () => {
|
|
221
|
+
await api('POST', '/openclaw/skills/remove', { skillKey: removeTarget.skillKey, source: removeTarget.source })
|
|
222
|
+
setRemoveTarget(null)
|
|
223
|
+
loadSkills()
|
|
224
|
+
}}
|
|
225
|
+
onCancel={() => setRemoveTarget(null)}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { PermissionPreset } from '@/types'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
agentId: string
|
|
9
|
+
onPresetChanged?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PRESETS: { id: PermissionPreset; label: string; desc: string; color: string }[] = [
|
|
13
|
+
{ id: 'conservative', label: 'Conservative', desc: 'Block all exec, no tools', color: 'text-red-400 bg-red-400/[0.08] border-red-400/20' },
|
|
14
|
+
{ id: 'collaborative', label: 'Collaborative', desc: 'Allowlist exec, web + fs tools', color: 'text-amber-300 bg-amber-400/[0.08] border-amber-400/20' },
|
|
15
|
+
{ id: 'autonomous', label: 'Autonomous', desc: 'Full exec, all tools', color: 'text-emerald-400 bg-emerald-400/[0.08] border-emerald-400/20' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function PermissionPresetSelector({ agentId, onPresetChanged }: Props) {
|
|
19
|
+
const [current, setCurrent] = useState<PermissionPreset | 'custom' | null>(null)
|
|
20
|
+
const [loading, setLoading] = useState(true)
|
|
21
|
+
const [applying, setApplying] = useState(false)
|
|
22
|
+
|
|
23
|
+
const load = useCallback(async () => {
|
|
24
|
+
setLoading(true)
|
|
25
|
+
try {
|
|
26
|
+
const res = await api<{ preset: PermissionPreset | 'custom' }>('GET', `/openclaw/permissions?agentId=${agentId}`)
|
|
27
|
+
setCurrent(res.preset)
|
|
28
|
+
} catch {
|
|
29
|
+
setCurrent(null)
|
|
30
|
+
} finally {
|
|
31
|
+
setLoading(false)
|
|
32
|
+
}
|
|
33
|
+
}, [agentId])
|
|
34
|
+
|
|
35
|
+
useEffect(() => { load() }, [load])
|
|
36
|
+
|
|
37
|
+
const handleSelect = async (preset: PermissionPreset) => {
|
|
38
|
+
if (applying || preset === current) return
|
|
39
|
+
setApplying(true)
|
|
40
|
+
try {
|
|
41
|
+
await api('PUT', '/openclaw/permissions', { agentId, preset })
|
|
42
|
+
setCurrent(preset)
|
|
43
|
+
onPresetChanged?.()
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
} finally {
|
|
47
|
+
setApplying(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (loading) return <div className="text-[12px] text-text-3/50 py-2">Loading presets...</div>
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-2">
|
|
55
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">Permission Preset</label>
|
|
56
|
+
<div className="flex gap-2">
|
|
57
|
+
{PRESETS.map((p) => (
|
|
58
|
+
<button
|
|
59
|
+
key={p.id}
|
|
60
|
+
onClick={() => handleSelect(p.id)}
|
|
61
|
+
disabled={applying}
|
|
62
|
+
className={`flex-1 flex flex-col items-center gap-1 py-2.5 px-2 rounded-[10px] border cursor-pointer transition-all
|
|
63
|
+
${current === p.id
|
|
64
|
+
? p.color
|
|
65
|
+
: 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:border-white/[0.12]'
|
|
66
|
+
} ${applying ? 'opacity-50' : ''}`}
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
<span className="text-[11px] font-600">{p.label}</span>
|
|
70
|
+
<span className="text-[9px] text-text-3/50 text-center leading-tight">{p.desc}</span>
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
{current === 'custom' && (
|
|
75
|
+
<span className="text-[10px] text-text-3/50">Custom configuration — select a preset to override</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { PersonalityDraft } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import {
|
|
7
|
+
parseIdentityMd, serializeIdentityMd,
|
|
8
|
+
parseUserMd, serializeUserMd,
|
|
9
|
+
parseSoulMd, serializeSoulMd,
|
|
10
|
+
} from '@/lib/personality-parser'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
agentId: string
|
|
14
|
+
fileType: 'IDENTITY.md' | 'USER.md' | 'SOUL.md'
|
|
15
|
+
content: string
|
|
16
|
+
onSave: (content: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
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'
|
|
20
|
+
const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1'
|
|
21
|
+
|
|
22
|
+
export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
|
|
23
|
+
const [draft, setDraft] = useState<Record<string, string>>({})
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (fileType === 'IDENTITY.md') {
|
|
27
|
+
const parsed = parseIdentityMd(content)
|
|
28
|
+
setDraft({ name: parsed.name || '', creature: parsed.creature || '', vibe: parsed.vibe || '', emoji: parsed.emoji || '' })
|
|
29
|
+
} else if (fileType === 'USER.md') {
|
|
30
|
+
const parsed = parseUserMd(content)
|
|
31
|
+
setDraft({ name: parsed.name || '', callThem: parsed.callThem || '', pronouns: parsed.pronouns || '', timezone: parsed.timezone || '', notes: parsed.notes || '', context: parsed.context || '' })
|
|
32
|
+
} else if (fileType === 'SOUL.md') {
|
|
33
|
+
const parsed = parseSoulMd(content)
|
|
34
|
+
setDraft({ coreTruths: parsed.coreTruths || '', boundaries: parsed.boundaries || '', vibe: parsed.vibe || '', continuity: parsed.continuity || '' })
|
|
35
|
+
}
|
|
36
|
+
}, [content, fileType])
|
|
37
|
+
|
|
38
|
+
const update = (key: string, value: string) => {
|
|
39
|
+
setDraft((prev) => ({ ...prev, [key]: value }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleSave = () => {
|
|
43
|
+
let serialized = ''
|
|
44
|
+
if (fileType === 'IDENTITY.md') {
|
|
45
|
+
serialized = serializeIdentityMd(draft as PersonalityDraft['identity'])
|
|
46
|
+
} else if (fileType === 'USER.md') {
|
|
47
|
+
serialized = serializeUserMd(draft as PersonalityDraft['user'])
|
|
48
|
+
} else if (fileType === 'SOUL.md') {
|
|
49
|
+
serialized = serializeSoulMd(draft as PersonalityDraft['soul'])
|
|
50
|
+
}
|
|
51
|
+
onSave(serialized)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fields = fileType === 'IDENTITY.md'
|
|
55
|
+
? [
|
|
56
|
+
{ key: 'name', label: 'Name', placeholder: 'Agent display name' },
|
|
57
|
+
{ key: 'creature', label: 'Creature / Type', placeholder: 'e.g. fox, robot, wizard' },
|
|
58
|
+
{ key: 'vibe', label: 'Vibe', placeholder: 'e.g. calm, energetic, mysterious' },
|
|
59
|
+
{ key: 'emoji', label: 'Emoji / Icon', placeholder: 'e.g. a single emoji' },
|
|
60
|
+
]
|
|
61
|
+
: fileType === 'USER.md'
|
|
62
|
+
? [
|
|
63
|
+
{ key: 'name', label: 'User Name', placeholder: 'Your name' },
|
|
64
|
+
{ key: 'callThem', label: 'Call Them', placeholder: 'Nickname / preferred name' },
|
|
65
|
+
{ key: 'pronouns', label: 'Pronouns', placeholder: 'e.g. they/them' },
|
|
66
|
+
{ key: 'timezone', label: 'Timezone', placeholder: 'e.g. America/New_York' },
|
|
67
|
+
{ key: 'notes', label: 'Notes', placeholder: 'Quick notes' },
|
|
68
|
+
{ key: 'context', label: 'Context', placeholder: 'Additional context...', multiline: true },
|
|
69
|
+
]
|
|
70
|
+
: [
|
|
71
|
+
{ key: 'coreTruths', label: 'Core Truths', placeholder: 'What the agent believes...', multiline: true },
|
|
72
|
+
{ key: 'boundaries', label: 'Boundaries', placeholder: 'What the agent won\'t do...', multiline: true },
|
|
73
|
+
{ key: 'vibe', label: 'Vibe', placeholder: 'Personality tone and style...', multiline: true },
|
|
74
|
+
{ key: 'continuity', label: 'Continuity', placeholder: 'What persists between sessions...', multiline: true },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex flex-col gap-3">
|
|
79
|
+
{fields.map((f) => (
|
|
80
|
+
<div key={f.key}>
|
|
81
|
+
<label className={labelClass}>{f.label}</label>
|
|
82
|
+
{'multiline' in f && f.multiline ? (
|
|
83
|
+
<textarea
|
|
84
|
+
value={draft[f.key] || ''}
|
|
85
|
+
onChange={(e) => update(f.key, e.target.value)}
|
|
86
|
+
placeholder={f.placeholder}
|
|
87
|
+
rows={3}
|
|
88
|
+
className={`${inputClass} resize-none`}
|
|
89
|
+
style={{ fontFamily: 'ui-monospace, monospace' }}
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
value={draft[f.key] || ''}
|
|
95
|
+
onChange={(e) => update(f.key, e.target.value)}
|
|
96
|
+
placeholder={f.placeholder}
|
|
97
|
+
className={inputClass}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
<button
|
|
103
|
+
onClick={handleSave}
|
|
104
|
+
className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110"
|
|
105
|
+
style={{ fontFamily: 'inherit' }}
|
|
106
|
+
>
|
|
107
|
+
Apply to Raw Editor
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
|
|
6
|
+
export function SandboxEnvPanel() {
|
|
7
|
+
const [available, setAvailable] = useState<string[]>([])
|
|
8
|
+
const [allowed, setAllowed] = useState<Set<string>>(new Set())
|
|
9
|
+
const [loading, setLoading] = useState(true)
|
|
10
|
+
const [saving, setSaving] = useState(false)
|
|
11
|
+
const [error, setError] = useState('')
|
|
12
|
+
|
|
13
|
+
const load = useCallback(async () => {
|
|
14
|
+
setLoading(true)
|
|
15
|
+
setError('')
|
|
16
|
+
try {
|
|
17
|
+
const res = await api<{ available: string[]; allowed: string[] }>('GET', '/openclaw/sandbox-env')
|
|
18
|
+
setAvailable(res.available)
|
|
19
|
+
setAllowed(new Set(res.allowed))
|
|
20
|
+
} catch (err: unknown) {
|
|
21
|
+
setError(err instanceof Error ? err.message : 'Failed to load')
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false)
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
useEffect(() => { load() }, [load])
|
|
28
|
+
|
|
29
|
+
const toggle = async (key: string) => {
|
|
30
|
+
const next = new Set(allowed)
|
|
31
|
+
if (next.has(key)) next.delete(key)
|
|
32
|
+
else next.add(key)
|
|
33
|
+
setAllowed(next)
|
|
34
|
+
|
|
35
|
+
setSaving(true)
|
|
36
|
+
setError('')
|
|
37
|
+
try {
|
|
38
|
+
await api('PUT', '/openclaw/sandbox-env', { allowed: Array.from(next) })
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
setError(err instanceof Error ? err.message : 'Save failed')
|
|
41
|
+
} finally {
|
|
42
|
+
setSaving(false)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (loading) return <div className="text-[12px] text-text-3/50 py-2">Loading env keys...</div>
|
|
47
|
+
|
|
48
|
+
if (!available.length) {
|
|
49
|
+
return <div className="text-[12px] text-text-3/50 py-2">No .env keys found on gateway.</div>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex flex-col gap-2">
|
|
54
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">Sandbox Env Allowlist</label>
|
|
55
|
+
<div className="flex flex-col gap-1">
|
|
56
|
+
{available.map((key) => (
|
|
57
|
+
<label key={key} className="flex items-center gap-2 py-1 px-2 rounded-[8px] hover:bg-white/[0.02] cursor-pointer transition-colors">
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
checked={allowed.has(key)}
|
|
61
|
+
onChange={() => toggle(key)}
|
|
62
|
+
disabled={saving}
|
|
63
|
+
className="accent-accent-bright"
|
|
64
|
+
/>
|
|
65
|
+
<span className="text-[12px] font-mono text-text">{key}</span>
|
|
66
|
+
</label>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import type { SkillInstallOption } from '@/types'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
open: boolean
|
|
10
|
+
onClose: () => void
|
|
11
|
+
skillName: string
|
|
12
|
+
installOptions?: SkillInstallOption[]
|
|
13
|
+
onInstalled: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SkillInstallDialog({ open, onClose, skillName, installOptions = [], onInstalled }: Props) {
|
|
17
|
+
const [selectedMethod, setSelectedMethod] = useState<string>(installOptions[0]?.kind || 'brew')
|
|
18
|
+
const [installing, setInstalling] = useState(false)
|
|
19
|
+
const [error, setError] = useState('')
|
|
20
|
+
const [progress, setProgress] = useState('')
|
|
21
|
+
|
|
22
|
+
const handleInstall = async () => {
|
|
23
|
+
setInstalling(true)
|
|
24
|
+
setError('')
|
|
25
|
+
setProgress('Installing...')
|
|
26
|
+
try {
|
|
27
|
+
await api('POST', '/openclaw/skills/install', {
|
|
28
|
+
name: skillName,
|
|
29
|
+
installId: selectedMethod,
|
|
30
|
+
timeoutMs: 120_000,
|
|
31
|
+
})
|
|
32
|
+
setProgress('Installed successfully!')
|
|
33
|
+
onInstalled()
|
|
34
|
+
setTimeout(onClose, 1000)
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
setError(err instanceof Error ? err.message : 'Installation failed')
|
|
37
|
+
setProgress('')
|
|
38
|
+
} finally {
|
|
39
|
+
setInstalling(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const methods = installOptions.length > 0
|
|
44
|
+
? installOptions
|
|
45
|
+
: [
|
|
46
|
+
{ kind: 'brew' as const, label: 'Homebrew' },
|
|
47
|
+
{ kind: 'node' as const, label: 'npm/Node' },
|
|
48
|
+
{ kind: 'go' as const, label: 'Go install' },
|
|
49
|
+
{ kind: 'uv' as const, label: 'UV (Python)' },
|
|
50
|
+
{ kind: 'download' as const, label: 'Direct download' },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
55
|
+
<DialogContent className="sm:max-w-[400px]">
|
|
56
|
+
<DialogHeader>
|
|
57
|
+
<DialogTitle>Install {skillName}</DialogTitle>
|
|
58
|
+
</DialogHeader>
|
|
59
|
+
<div className="py-3 flex flex-col gap-3">
|
|
60
|
+
<label className="text-[12px] font-600 text-text-3">Install Method</label>
|
|
61
|
+
<div className="flex flex-wrap gap-2">
|
|
62
|
+
{methods.map((m) => (
|
|
63
|
+
<button
|
|
64
|
+
key={m.kind}
|
|
65
|
+
onClick={() => setSelectedMethod(m.kind)}
|
|
66
|
+
disabled={installing}
|
|
67
|
+
className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
68
|
+
${selectedMethod === m.kind
|
|
69
|
+
? 'bg-accent-soft text-accent-bright border-accent-bright/30'
|
|
70
|
+
: 'bg-transparent text-text-3 border-white/[0.08] hover:border-white/[0.15]'
|
|
71
|
+
}`}
|
|
72
|
+
style={{ fontFamily: 'inherit' }}
|
|
73
|
+
>
|
|
74
|
+
{m.label}
|
|
75
|
+
</button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
{progress && <p className="text-[12px] text-emerald-400">{progress}</p>}
|
|
79
|
+
{error && <p className="text-[12px] text-red-400">{error}</p>}
|
|
80
|
+
</div>
|
|
81
|
+
<DialogFooter>
|
|
82
|
+
<button
|
|
83
|
+
onClick={onClose}
|
|
84
|
+
disabled={installing}
|
|
85
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
86
|
+
style={{ fontFamily: 'inherit' }}
|
|
87
|
+
>
|
|
88
|
+
Cancel
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleInstall}
|
|
92
|
+
disabled={installing}
|
|
93
|
+
className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer disabled:opacity-40 transition-all hover:brightness-110"
|
|
94
|
+
style={{ fontFamily: 'inherit' }}
|
|
95
|
+
>
|
|
96
|
+
{installing ? 'Installing...' : 'Install'}
|
|
97
|
+
</button>
|
|
98
|
+
</DialogFooter>
|
|
99
|
+
</DialogContent>
|
|
100
|
+
</Dialog>
|
|
101
|
+
)
|
|
102
|
+
}
|