@swarmclawai/swarmclaw 0.7.7 → 0.7.8
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 +10 -9
- package/package.json +1 -1
- package/src/app/api/chats/route.ts +1 -0
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/components/agents/agent-sheet.tsx +184 -14
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +25 -1
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +234 -7
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/types/index.ts +39 -0
|
@@ -12,6 +12,25 @@ const PROJECT_COLORS = [
|
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
const inputClass = 'w-full px-3 py-2.5 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40 transition-colors'
|
|
15
|
+
const sectionTitleClass = 'block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2'
|
|
16
|
+
|
|
17
|
+
function listToText(values?: string[]) {
|
|
18
|
+
return Array.isArray(values) ? values.join('\n') : ''
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function textToList(value: string) {
|
|
22
|
+
return value
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((entry) => entry.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseOptionalInteger(value: string) {
|
|
29
|
+
const trimmed = value.trim()
|
|
30
|
+
if (!trimmed) return undefined
|
|
31
|
+
const parsed = Number.parseInt(trimmed, 10)
|
|
32
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
33
|
+
}
|
|
15
34
|
|
|
16
35
|
export function ProjectSheet() {
|
|
17
36
|
const open = useAppStore((s) => s.projectSheetOpen)
|
|
@@ -24,6 +43,15 @@ export function ProjectSheet() {
|
|
|
24
43
|
const [name, setName] = useState('')
|
|
25
44
|
const [description, setDescription] = useState('')
|
|
26
45
|
const [color, setColor] = useState<string | undefined>(undefined)
|
|
46
|
+
const [objective, setObjective] = useState('')
|
|
47
|
+
const [audience, setAudience] = useState('')
|
|
48
|
+
const [prioritiesText, setPrioritiesText] = useState('')
|
|
49
|
+
const [openObjectivesText, setOpenObjectivesText] = useState('')
|
|
50
|
+
const [capabilityHintsText, setCapabilityHintsText] = useState('')
|
|
51
|
+
const [credentialRequirementsText, setCredentialRequirementsText] = useState('')
|
|
52
|
+
const [successMetricsText, setSuccessMetricsText] = useState('')
|
|
53
|
+
const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
|
|
54
|
+
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('')
|
|
27
55
|
|
|
28
56
|
const editing = editingId ? projects[editingId] : null
|
|
29
57
|
|
|
@@ -33,10 +61,28 @@ export function ProjectSheet() {
|
|
|
33
61
|
setName(editing.name)
|
|
34
62
|
setDescription(editing.description)
|
|
35
63
|
setColor(editing.color)
|
|
64
|
+
setObjective(editing.objective || '')
|
|
65
|
+
setAudience(editing.audience || '')
|
|
66
|
+
setPrioritiesText(listToText(editing.priorities))
|
|
67
|
+
setOpenObjectivesText(listToText(editing.openObjectives))
|
|
68
|
+
setCapabilityHintsText(listToText(editing.capabilityHints))
|
|
69
|
+
setCredentialRequirementsText(listToText(editing.credentialRequirements))
|
|
70
|
+
setSuccessMetricsText(listToText(editing.successMetrics))
|
|
71
|
+
setHeartbeatPrompt(editing.heartbeatPrompt || '')
|
|
72
|
+
setHeartbeatIntervalSec(editing.heartbeatIntervalSec ? String(editing.heartbeatIntervalSec) : '')
|
|
36
73
|
} else {
|
|
37
74
|
setName('')
|
|
38
75
|
setDescription('')
|
|
39
76
|
setColor(PROJECT_COLORS[0])
|
|
77
|
+
setObjective('')
|
|
78
|
+
setAudience('')
|
|
79
|
+
setPrioritiesText('')
|
|
80
|
+
setOpenObjectivesText('')
|
|
81
|
+
setCapabilityHintsText('')
|
|
82
|
+
setCredentialRequirementsText('')
|
|
83
|
+
setSuccessMetricsText('')
|
|
84
|
+
setHeartbeatPrompt('')
|
|
85
|
+
setHeartbeatIntervalSec('')
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
88
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -52,6 +98,15 @@ export function ProjectSheet() {
|
|
|
52
98
|
name: name.trim() || 'Unnamed Project',
|
|
53
99
|
description,
|
|
54
100
|
color,
|
|
101
|
+
objective: objective.trim() || undefined,
|
|
102
|
+
audience: audience.trim() || undefined,
|
|
103
|
+
priorities: textToList(prioritiesText),
|
|
104
|
+
openObjectives: textToList(openObjectivesText),
|
|
105
|
+
capabilityHints: textToList(capabilityHintsText),
|
|
106
|
+
credentialRequirements: textToList(credentialRequirementsText),
|
|
107
|
+
successMetrics: textToList(successMetricsText),
|
|
108
|
+
heartbeatPrompt: heartbeatPrompt.trim() || undefined,
|
|
109
|
+
heartbeatIntervalSec: parseOptionalInteger(heartbeatIntervalSec),
|
|
55
110
|
}
|
|
56
111
|
if (editing) {
|
|
57
112
|
await updateProject(editing.id, data)
|
|
@@ -72,10 +127,10 @@ export function ProjectSheet() {
|
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
return (
|
|
75
|
-
<BottomSheet open={open} onClose={onClose}>
|
|
130
|
+
<BottomSheet open={open} onClose={onClose} wide>
|
|
76
131
|
<h2 className="font-display text-[18px] font-700 text-text mb-6">{editing ? 'Edit Project' : 'New Project'}</h2>
|
|
77
132
|
<div className="mb-6">
|
|
78
|
-
<label className=
|
|
133
|
+
<label className={sectionTitleClass}>Name</label>
|
|
79
134
|
<input
|
|
80
135
|
type="text"
|
|
81
136
|
value={name}
|
|
@@ -88,7 +143,7 @@ export function ProjectSheet() {
|
|
|
88
143
|
</div>
|
|
89
144
|
|
|
90
145
|
<div className="mb-6">
|
|
91
|
-
<label className=
|
|
146
|
+
<label className={sectionTitleClass}>Description</label>
|
|
92
147
|
<textarea
|
|
93
148
|
value={description}
|
|
94
149
|
onChange={(e) => setDescription(e.target.value)}
|
|
@@ -99,8 +154,125 @@ export function ProjectSheet() {
|
|
|
99
154
|
/>
|
|
100
155
|
</div>
|
|
101
156
|
|
|
157
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
158
|
+
<div>
|
|
159
|
+
<label className={sectionTitleClass}>Objective</label>
|
|
160
|
+
<textarea
|
|
161
|
+
value={objective}
|
|
162
|
+
onChange={(e) => setObjective(e.target.value)}
|
|
163
|
+
placeholder="What durable outcome is this project driving?"
|
|
164
|
+
className={inputClass + ' min-h-[88px] resize-y'}
|
|
165
|
+
style={{ fontFamily: 'inherit' }}
|
|
166
|
+
rows={4}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<div>
|
|
170
|
+
<label className={sectionTitleClass}>Audience</label>
|
|
171
|
+
<textarea
|
|
172
|
+
value={audience}
|
|
173
|
+
onChange={(e) => setAudience(e.target.value)}
|
|
174
|
+
placeholder="Who is this project for?"
|
|
175
|
+
className={inputClass + ' min-h-[88px] resize-y'}
|
|
176
|
+
style={{ fontFamily: 'inherit' }}
|
|
177
|
+
rows={4}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
183
|
+
<div>
|
|
184
|
+
<label className={sectionTitleClass}>Pilot Priorities</label>
|
|
185
|
+
<textarea
|
|
186
|
+
value={prioritiesText}
|
|
187
|
+
onChange={(e) => setPrioritiesText(e.target.value)}
|
|
188
|
+
placeholder={'One per line\nResearch the market\nBuild the pilot'}
|
|
189
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
190
|
+
style={{ fontFamily: 'inherit' }}
|
|
191
|
+
rows={5}
|
|
192
|
+
/>
|
|
193
|
+
<p className="mt-2 text-[11px] text-text-3/45">One priority per line.</p>
|
|
194
|
+
</div>
|
|
195
|
+
<div>
|
|
196
|
+
<label className={sectionTitleClass}>Open Objectives</label>
|
|
197
|
+
<textarea
|
|
198
|
+
value={openObjectivesText}
|
|
199
|
+
onChange={(e) => setOpenObjectivesText(e.target.value)}
|
|
200
|
+
placeholder={'One per line\nDraft the research brief\nPrepare the rollout checklist'}
|
|
201
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
202
|
+
style={{ fontFamily: 'inherit' }}
|
|
203
|
+
rows={5}
|
|
204
|
+
/>
|
|
205
|
+
<p className="mt-2 text-[11px] text-text-3/45">Use this for durable next outcomes, not one-off chat prompts.</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
210
|
+
<div>
|
|
211
|
+
<label className={sectionTitleClass}>Capability Hints</label>
|
|
212
|
+
<textarea
|
|
213
|
+
value={capabilityHintsText}
|
|
214
|
+
onChange={(e) => setCapabilityHintsText(e.target.value)}
|
|
215
|
+
placeholder={'One per line\nResearch\nWeb browsing\nInbox automation'}
|
|
216
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
217
|
+
style={{ fontFamily: 'inherit' }}
|
|
218
|
+
rows={5}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<label className={sectionTitleClass}>Credential Requirements</label>
|
|
223
|
+
<textarea
|
|
224
|
+
value={credentialRequirementsText}
|
|
225
|
+
onChange={(e) => setCredentialRequirementsText(e.target.value)}
|
|
226
|
+
placeholder={'One per line\nGmail app password\nCRM API token'}
|
|
227
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
228
|
+
style={{ fontFamily: 'inherit' }}
|
|
229
|
+
rows={5}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
235
|
+
<div>
|
|
236
|
+
<label className={sectionTitleClass}>Success Metrics</label>
|
|
237
|
+
<textarea
|
|
238
|
+
value={successMetricsText}
|
|
239
|
+
onChange={(e) => setSuccessMetricsText(e.target.value)}
|
|
240
|
+
placeholder={'One per line\nReduce response time below 10 minutes\nIncrease qualified replies'}
|
|
241
|
+
className={inputClass + ' min-h-[96px] resize-y'}
|
|
242
|
+
style={{ fontFamily: 'inherit' }}
|
|
243
|
+
rows={4}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="grid gap-4">
|
|
247
|
+
<div>
|
|
248
|
+
<label className={sectionTitleClass}>Heartbeat Prompt</label>
|
|
249
|
+
<textarea
|
|
250
|
+
value={heartbeatPrompt}
|
|
251
|
+
onChange={(e) => setHeartbeatPrompt(e.target.value)}
|
|
252
|
+
placeholder="What should the project heartbeat ask the agent to review?"
|
|
253
|
+
className={inputClass + ' min-h-[72px] resize-y'}
|
|
254
|
+
style={{ fontFamily: 'inherit' }}
|
|
255
|
+
rows={3}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
<div>
|
|
259
|
+
<label className={sectionTitleClass}>Heartbeat Interval (seconds)</label>
|
|
260
|
+
<input
|
|
261
|
+
type="number"
|
|
262
|
+
min={0}
|
|
263
|
+
step={60}
|
|
264
|
+
value={heartbeatIntervalSec}
|
|
265
|
+
onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
|
|
266
|
+
placeholder="1800"
|
|
267
|
+
className={inputClass}
|
|
268
|
+
style={{ fontFamily: 'inherit' }}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
102
274
|
<div className="mb-8">
|
|
103
|
-
<label className=
|
|
275
|
+
<label className={sectionTitleClass}>Color</label>
|
|
104
276
|
<div className="flex items-center gap-2">
|
|
105
277
|
{PROJECT_COLORS.map((c) => (
|
|
106
278
|
<button
|
|
@@ -46,6 +46,44 @@ export function CapabilityPolicySection({ appSettings, patchSettings, inputClass
|
|
|
46
46
|
</div>
|
|
47
47
|
|
|
48
48
|
<div className="grid grid-cols-1 gap-4">
|
|
49
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
50
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
51
|
+
<div className="flex items-center justify-between gap-4">
|
|
52
|
+
<div>
|
|
53
|
+
<div className="text-[12px] font-600 text-text-2">Task Management</div>
|
|
54
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
55
|
+
Controls the task board and agent access to durable backlog tracking. Internal queue execution still works underneath.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => patchSettings({ taskManagementEnabled: !(appSettings.taskManagementEnabled ?? true) })}
|
|
60
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.taskManagementEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
61
|
+
aria-label="Toggle task management"
|
|
62
|
+
>
|
|
63
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.taskManagementEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
69
|
+
<div className="flex items-center justify-between gap-4">
|
|
70
|
+
<div>
|
|
71
|
+
<div className="text-[12px] font-600 text-text-2">Project Management</div>
|
|
72
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
73
|
+
Controls the project operating-system UI and agent access to durable project context for objectives, credentials, and heartbeat plans.
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => patchSettings({ projectManagementEnabled: !(appSettings.projectManagementEnabled ?? true) })}
|
|
78
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${(appSettings.projectManagementEnabled ?? true) ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
79
|
+
aria-label="Toggle project management"
|
|
80
|
+
>
|
|
81
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${(appSettings.projectManagementEnabled ?? true) ? 'translate-x-[18px]' : ''}`} />
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
49
87
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
50
88
|
<div className="flex items-center justify-between gap-4">
|
|
51
89
|
<div>
|
|
@@ -5,6 +5,8 @@ import type { SettingsSectionProps } from './types'
|
|
|
5
5
|
export function VoiceSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
6
6
|
const enabled = appSettings.elevenLabsEnabled ?? false
|
|
7
7
|
const hasApiKey = appSettings.elevenLabsApiKeyConfigured === true
|
|
8
|
+
const defaultVoiceId = typeof appSettings.elevenLabsVoiceId === 'string' ? appSettings.elevenLabsVoiceId.trim() : ''
|
|
9
|
+
const showVoiceConfig = enabled || hasApiKey || Boolean(defaultVoiceId)
|
|
8
10
|
|
|
9
11
|
return (
|
|
10
12
|
<div className="mb-10">
|
|
@@ -12,7 +14,7 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
|
|
|
12
14
|
Voice
|
|
13
15
|
</h3>
|
|
14
16
|
<p className="text-[12px] text-text-3 mb-5">
|
|
15
|
-
Configure voice playback (TTS) and speech-to-text input.
|
|
17
|
+
Configure voice playback (TTS), the default ElevenLabs voice, and speech-to-text input.
|
|
16
18
|
</p>
|
|
17
19
|
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
18
20
|
{/* ElevenLabs toggle */}
|
|
@@ -30,7 +32,7 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
|
|
|
30
32
|
</button>
|
|
31
33
|
</div>
|
|
32
34
|
|
|
33
|
-
{
|
|
35
|
+
{showVoiceConfig && (
|
|
34
36
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
|
|
35
37
|
<div>
|
|
36
38
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">API Key</label>
|
|
@@ -56,11 +58,17 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
|
|
|
56
58
|
className={inputClass}
|
|
57
59
|
style={{ fontFamily: 'inherit' }}
|
|
58
60
|
/>
|
|
59
|
-
<p className="text-[11px] text-text-3/60 mt-1.5">Fallback voice when an agent has no override set.</p>
|
|
61
|
+
<p className="text-[11px] text-text-3/60 mt-1.5">Fallback voice when an agent has no override set. Agents can override this in their own create/edit sheet.</p>
|
|
60
62
|
</div>
|
|
61
63
|
</div>
|
|
62
64
|
)}
|
|
63
65
|
|
|
66
|
+
{showVoiceConfig && !enabled && (
|
|
67
|
+
<p className="mb-5 rounded-[12px] border border-white/[0.06] bg-white/[0.03] px-3 py-2.5 text-[11px] text-text-3/70">
|
|
68
|
+
ElevenLabs credentials and default voice can be prepared here even while playback is turned off.
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
|
|
64
72
|
<div>
|
|
65
73
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Speech Recognition Language</label>
|
|
66
74
|
<input
|
|
@@ -8,7 +8,7 @@ import { toast } from 'sonner'
|
|
|
8
8
|
import { useWs } from '@/hooks/use-ws'
|
|
9
9
|
import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
|
|
10
10
|
import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
|
|
11
|
-
import type { ApprovalRequest } from '@/types'
|
|
11
|
+
import type { AppSettings, ApprovalCategory, ApprovalRequest } from '@/types'
|
|
12
12
|
|
|
13
13
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
14
14
|
tool_access: 'Plugin Access',
|
|
@@ -28,6 +28,15 @@ const CATEGORY_ICONS: Record<string, string> = {
|
|
|
28
28
|
|
|
29
29
|
type ApprovalScope = 'all' | 'execution' | 'workflow' | 'task'
|
|
30
30
|
|
|
31
|
+
const AUTO_APPROVE_OPTIONS: Array<{ id: ApprovalCategory; label: string; description: string }> = [
|
|
32
|
+
{ id: 'tool_access', label: 'Plugin Access', description: 'Auto-enable requested plugins for a chat.' },
|
|
33
|
+
{ id: 'plugin_scaffold', label: 'Plugin Scaffold', description: 'Auto-create plugin files requested by agents.' },
|
|
34
|
+
{ id: 'plugin_install', label: 'Plugin Install', description: 'Auto-install plugins from approved URLs.' },
|
|
35
|
+
{ id: 'human_loop', label: 'Human Approval Requests', description: 'Auto-approve ask-human approval prompts.' },
|
|
36
|
+
{ id: 'wallet_transfer', label: 'Wallet Transfers', description: 'Auto-approve wallet send requests. High risk.' },
|
|
37
|
+
{ id: 'task_tool', label: 'Task Tool Calls', description: 'Auto-approve task-level tool approvals.' },
|
|
38
|
+
]
|
|
39
|
+
|
|
31
40
|
function relativeTime(ts: number): string {
|
|
32
41
|
const diff = Date.now() - ts
|
|
33
42
|
if (diff < 60_000) return 'just now'
|
|
@@ -39,9 +48,11 @@ function relativeTime(ts: number): string {
|
|
|
39
48
|
export function ApprovalsPanel() {
|
|
40
49
|
const tasks = useAppStore((s) => s.tasks)
|
|
41
50
|
const agents = useAppStore((s) => s.agents)
|
|
51
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
42
52
|
const serverApprovals = useAppStore((s) => s.approvals)
|
|
43
53
|
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
44
54
|
const loadServerApprovals = useAppStore((s) => s.loadApprovals)
|
|
55
|
+
const loadAppSettings = useAppStore((s) => s.loadSettings)
|
|
45
56
|
|
|
46
57
|
const execApprovals = useApprovalStore((s) => s.approvals)
|
|
47
58
|
const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
|
|
@@ -73,12 +84,17 @@ export function ApprovalsPanel() {
|
|
|
73
84
|
const [scope, setScope] = useState<ApprovalScope>('all')
|
|
74
85
|
const [categoryFilter, setCategoryFilter] = useState('all')
|
|
75
86
|
const [now, setNow] = useState(() => Date.now())
|
|
87
|
+
const [savingSetting, setSavingSetting] = useState<string | null>(null)
|
|
76
88
|
|
|
77
89
|
useEffect(() => {
|
|
78
90
|
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
79
91
|
return () => window.clearInterval(intervalId)
|
|
80
92
|
}, [])
|
|
81
93
|
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
void loadAppSettings()
|
|
96
|
+
}, [loadAppSettings])
|
|
97
|
+
|
|
82
98
|
const taskApprovals = useMemo(() => {
|
|
83
99
|
return Object.values(tasks)
|
|
84
100
|
.filter((t) => t.pendingApproval)
|
|
@@ -180,6 +196,23 @@ export function ApprovalsPanel() {
|
|
|
180
196
|
},
|
|
181
197
|
]
|
|
182
198
|
|
|
199
|
+
const autoApproved = useMemo(() => new Set(appSettings.approvalAutoApproveCategories || []), [appSettings.approvalAutoApproveCategories])
|
|
200
|
+
const approvalsEnabled = appSettings.approvalsEnabled ?? true
|
|
201
|
+
const outboundApprovalEnabled = appSettings.safetyRequireApprovalForOutbound ?? false
|
|
202
|
+
|
|
203
|
+
const saveApprovalSettings = async (patch: Partial<AppSettings>, successMessage: string, key: string) => {
|
|
204
|
+
try {
|
|
205
|
+
setSavingSetting(key)
|
|
206
|
+
const settings = await api<AppSettings>('PUT', '/settings', patch)
|
|
207
|
+
useAppStore.setState({ appSettings: settings })
|
|
208
|
+
toast.success(successMessage)
|
|
209
|
+
} catch (err: unknown) {
|
|
210
|
+
toast.error(err instanceof Error ? err.message : 'Failed to update approval settings')
|
|
211
|
+
} finally {
|
|
212
|
+
setSavingSetting((current) => (current === key ? null : current))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
|
|
184
217
|
try {
|
|
185
218
|
if (req.category === 'task_tool') {
|
|
@@ -195,23 +228,6 @@ export function ApprovalsPanel() {
|
|
|
195
228
|
}
|
|
196
229
|
}
|
|
197
230
|
|
|
198
|
-
if (pendingCount === 0) {
|
|
199
|
-
return (
|
|
200
|
-
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
|
201
|
-
<div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
|
|
202
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
|
|
203
|
-
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
204
|
-
<path d="m9 12 2 2 4-4"/>
|
|
205
|
-
</svg>
|
|
206
|
-
</div>
|
|
207
|
-
<h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
|
|
208
|
-
<p className="text-[13px] text-text-3/60 max-w-[320px]">
|
|
209
|
-
Your swarm is operating autonomously. Actions requiring oversight will appear here.
|
|
210
|
-
</p>
|
|
211
|
-
</div>
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
231
|
return (
|
|
216
232
|
<div className="flex-1 overflow-y-auto px-6 py-8">
|
|
217
233
|
<div className="max-w-3xl mx-auto">
|
|
@@ -237,6 +253,132 @@ export function ApprovalsPanel() {
|
|
|
237
253
|
))}
|
|
238
254
|
</div>
|
|
239
255
|
|
|
256
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
|
|
257
|
+
<div className="flex flex-col gap-4">
|
|
258
|
+
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-3">
|
|
259
|
+
<div>
|
|
260
|
+
<h2 className="text-[13px] font-700 text-text">Approval Controls</h2>
|
|
261
|
+
<p className="text-[12px] text-text-3/70 mt-1 max-w-[640px]">
|
|
262
|
+
Control whether actions queue for review, which approval types auto-run, and whether outbound connector sends need explicit confirmation.
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
<div className={`px-3 py-1.5 rounded-full text-[11px] font-700 ${
|
|
266
|
+
approvalsEnabled
|
|
267
|
+
? 'bg-amber-500/10 border border-amber-500/20 text-amber-300'
|
|
268
|
+
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-300'
|
|
269
|
+
}`}>
|
|
270
|
+
{approvalsEnabled ? 'Manual approvals enabled' : 'Approvals disabled'}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
275
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-4 py-4">
|
|
276
|
+
<div className="flex items-center justify-between gap-4">
|
|
277
|
+
<div>
|
|
278
|
+
<div className="text-[12px] font-600 text-text-2">Platform Approvals</div>
|
|
279
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
280
|
+
Turn this off to auto-approve workflow approvals across the app. Audit records are still kept.
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
disabled={savingSetting === 'approvalsEnabled'}
|
|
286
|
+
onClick={() => {
|
|
287
|
+
const next = !approvalsEnabled
|
|
288
|
+
void saveApprovalSettings(
|
|
289
|
+
{ approvalsEnabled: next },
|
|
290
|
+
next ? 'Platform approvals enabled' : 'Platform approvals disabled',
|
|
291
|
+
'approvalsEnabled',
|
|
292
|
+
)
|
|
293
|
+
}}
|
|
294
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer disabled:opacity-50 ${approvalsEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
295
|
+
aria-label="Toggle platform approvals"
|
|
296
|
+
>
|
|
297
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${approvalsEnabled ? 'translate-x-[18px]' : ''}`} />
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-4 py-4">
|
|
303
|
+
<div className="flex items-center justify-between gap-4">
|
|
304
|
+
<div>
|
|
305
|
+
<div className="text-[12px] font-600 text-text-2">Outbound Send Approvals</div>
|
|
306
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
|
|
307
|
+
Require explicit approval before agents send messages or media over connectors.
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
disabled={savingSetting === 'safetyRequireApprovalForOutbound'}
|
|
313
|
+
onClick={() => {
|
|
314
|
+
const next = !outboundApprovalEnabled
|
|
315
|
+
void saveApprovalSettings(
|
|
316
|
+
{ safetyRequireApprovalForOutbound: next },
|
|
317
|
+
next ? 'Outbound send approvals enabled' : 'Outbound send approvals disabled',
|
|
318
|
+
'safetyRequireApprovalForOutbound',
|
|
319
|
+
)
|
|
320
|
+
}}
|
|
321
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer disabled:opacity-50 ${outboundApprovalEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
322
|
+
aria-label="Toggle outbound send approvals"
|
|
323
|
+
>
|
|
324
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${outboundApprovalEnabled ? 'translate-x-[18px]' : ''}`} />
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div>
|
|
331
|
+
<div className="flex items-center justify-between gap-3 mb-2">
|
|
332
|
+
<div className="text-[12px] font-600 text-text-2">Auto-Approve Categories</div>
|
|
333
|
+
<div className="text-[11px] text-text-3/60">
|
|
334
|
+
{autoApproved.size} enabled
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
338
|
+
{AUTO_APPROVE_OPTIONS.map((option) => {
|
|
339
|
+
const checked = autoApproved.has(option.id)
|
|
340
|
+
return (
|
|
341
|
+
<label
|
|
342
|
+
key={option.id}
|
|
343
|
+
className={`rounded-[12px] border px-3 py-3 cursor-pointer transition-all ${
|
|
344
|
+
checked
|
|
345
|
+
? 'border-accent-bright/30 bg-accent-soft/60'
|
|
346
|
+
: 'border-white/[0.06] bg-black/20 hover:bg-white/[0.04]'
|
|
347
|
+
}`}
|
|
348
|
+
>
|
|
349
|
+
<div className="flex items-start gap-3">
|
|
350
|
+
<input
|
|
351
|
+
type="checkbox"
|
|
352
|
+
checked={checked}
|
|
353
|
+
disabled={savingSetting === `auto:${option.id}`}
|
|
354
|
+
onChange={(e) => {
|
|
355
|
+
const next = new Set(appSettings.approvalAutoApproveCategories || [])
|
|
356
|
+
if (e.target.checked) next.add(option.id)
|
|
357
|
+
else next.delete(option.id)
|
|
358
|
+
void saveApprovalSettings(
|
|
359
|
+
{ approvalAutoApproveCategories: [...next] },
|
|
360
|
+
checked ? `${option.label} now requires approval` : `${option.label} will auto-approve`,
|
|
361
|
+
`auto:${option.id}`,
|
|
362
|
+
)
|
|
363
|
+
}}
|
|
364
|
+
className="mt-0.5"
|
|
365
|
+
/>
|
|
366
|
+
<div>
|
|
367
|
+
<div className="text-[12px] font-600 text-text-2">{option.label}</div>
|
|
368
|
+
<p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">{option.description}</p>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</label>
|
|
372
|
+
)
|
|
373
|
+
})}
|
|
374
|
+
</div>
|
|
375
|
+
<p className="text-[11px] text-text-3/60 mt-2">
|
|
376
|
+
Use category auto-approval when you still want the approval system on, but you do not want these request types to pause execution.
|
|
377
|
+
</p>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
240
382
|
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
|
|
241
383
|
<div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
|
|
242
384
|
<div className="flex flex-wrap gap-2">
|
|
@@ -411,6 +553,23 @@ export function ApprovalsPanel() {
|
|
|
411
553
|
<p className="text-[12px] text-text-3/60">Try clearing the search or switching the queue scope.</p>
|
|
412
554
|
</div>
|
|
413
555
|
)}
|
|
556
|
+
|
|
557
|
+
{pendingCount === 0 && (
|
|
558
|
+
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
559
|
+
<div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
|
|
560
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
|
|
561
|
+
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
562
|
+
<path d="m9 12 2 2 4-4"/>
|
|
563
|
+
</svg>
|
|
564
|
+
</div>
|
|
565
|
+
<h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
|
|
566
|
+
<p className="text-[13px] text-text-3/60 max-w-[360px]">
|
|
567
|
+
{approvalsEnabled
|
|
568
|
+
? 'Your swarm is operating autonomously. Actions requiring oversight will appear here.'
|
|
569
|
+
: 'Approvals are currently disabled, so eligible requests will auto-run instead of queuing here.'}
|
|
570
|
+
</p>
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
414
573
|
</div>
|
|
415
574
|
</div>
|
|
416
575
|
)
|