@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
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
151
|
-
To pin a version: `SWARMCLAW_VERSION=v0.7.
|
|
151
|
+
To pin a version: `SWARMCLAW_VERSION=v0.7.8 curl ... | bash`
|
|
152
152
|
|
|
153
153
|
Or run locally from the repo (friendly for non-technical users):
|
|
154
154
|
|
|
@@ -701,7 +701,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
701
701
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
702
702
|
|
|
703
703
|
```bash
|
|
704
|
-
# example patch release (v0.7.
|
|
704
|
+
# example patch release (v0.7.8 style)
|
|
705
705
|
npm version patch
|
|
706
706
|
git push origin main --follow-tags
|
|
707
707
|
```
|
|
@@ -711,15 +711,16 @@ On `v*` tags, GitHub Actions will:
|
|
|
711
711
|
2. Create a GitHub Release
|
|
712
712
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
713
713
|
|
|
714
|
-
#### v0.7.
|
|
714
|
+
#### v0.7.8 Release Readiness Notes
|
|
715
715
|
|
|
716
|
-
Before shipping `v0.7.
|
|
716
|
+
Before shipping `v0.7.8`, confirm the following user-facing changes are reflected in docs:
|
|
717
717
|
|
|
718
|
-
1.
|
|
719
|
-
2.
|
|
720
|
-
3.
|
|
721
|
-
4.
|
|
722
|
-
5.
|
|
718
|
+
1. Project docs explain the new project operating-system fields: objective, audience, pilot priorities, open objectives, credential requirements, success metrics, and heartbeat prompt/interval.
|
|
719
|
+
2. Task and approval docs cover the new approval controls, task/project management toggles, and durable task continuation behavior (`continueFromTaskId`, dependency blocking, and session resume reuse).
|
|
720
|
+
3. Connector/operator docs mention automatic connector recovery on disconnect or dev-server restart, including bounded exponential backoff instead of silent disablement.
|
|
721
|
+
4. Chat/runtime docs note the project-aware agent context, Gemini resume-handle visibility, and improved web/connector input handling where relevant.
|
|
722
|
+
5. Site and README install/version strings are updated to `v0.7.8`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
723
|
+
6. Release notes summarize the user-visible changes from the current worktree, especially project operating context, approval/task controls, connector resilience, and chat/runtime polish.
|
|
723
724
|
|
|
724
725
|
## CLI
|
|
725
726
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -2,8 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { loadConnectors, saveConnectors, logActivity } from '@/lib/server/storage'
|
|
3
3
|
import { notify } from '@/lib/server/ws-hub'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
|
|
6
7
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
ensureDaemonStarted('api/connectors/[id]:get')
|
|
7
9
|
const { id } = await params
|
|
8
10
|
const connectors = loadConnectors()
|
|
9
11
|
const connector = connectors[id]
|
|
@@ -11,8 +13,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
11
13
|
|
|
12
14
|
// Merge runtime status, QR code, and presence
|
|
13
15
|
try {
|
|
14
|
-
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence } = await import('@/lib/server/connectors/manager')
|
|
15
|
-
|
|
16
|
+
const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
17
|
+
const runtimeStatus = getConnectorStatus(id)
|
|
18
|
+
connector.status = runtimeStatus === 'running'
|
|
19
|
+
? 'running'
|
|
20
|
+
: connector.lastError
|
|
21
|
+
? 'error'
|
|
22
|
+
: 'stopped'
|
|
23
|
+
const rState = getReconnectState(id)
|
|
24
|
+
if (rState) {
|
|
25
|
+
const ext = connector as unknown as Record<string, unknown>
|
|
26
|
+
ext.reconnectAttempts = rState.attempts
|
|
27
|
+
ext.nextRetryAt = rState.nextRetryAt
|
|
28
|
+
ext.reconnectError = rState.error
|
|
29
|
+
ext.reconnectExhausted = rState.exhausted
|
|
30
|
+
}
|
|
16
31
|
const qr = getConnectorQR(id)
|
|
17
32
|
if (qr) connector.qrDataUrl = qr
|
|
18
33
|
connector.authenticated = isConnectorAuthenticated(id)
|
|
@@ -26,6 +41,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
44
|
+
ensureDaemonStarted('api/connectors/[id]:put')
|
|
29
45
|
const { id } = await params
|
|
30
46
|
const body = await req.json()
|
|
31
47
|
const connectors = loadConnectors()
|
|
@@ -38,12 +54,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
38
54
|
try {
|
|
39
55
|
const manager = await import('@/lib/server/connectors/manager')
|
|
40
56
|
if (body.action === 'start') {
|
|
57
|
+
manager.clearReconnectState(id)
|
|
41
58
|
await manager.startConnector(id)
|
|
42
59
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector started: "${connector.name}"` })
|
|
43
60
|
} else if (body.action === 'stop') {
|
|
44
61
|
await manager.stopConnector(id)
|
|
45
62
|
logActivity({ entityType: 'connector', entityId: id, action: 'stopped', actor: 'user', summary: `Connector stopped: "${connector.name}"` })
|
|
46
63
|
} else {
|
|
64
|
+
manager.clearReconnectState(id)
|
|
47
65
|
await manager.repairConnector(id)
|
|
48
66
|
logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector repaired: "${connector.name}"` })
|
|
49
67
|
}
|
|
@@ -2,19 +2,26 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadConnectors, saveConnectors } from '@/lib/server/storage'
|
|
4
4
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { ensureDaemonStarted } from '@/lib/server/daemon-state'
|
|
5
6
|
import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
6
7
|
import { z } from 'zod'
|
|
7
8
|
import type { Connector } from '@/types'
|
|
8
9
|
export const dynamic = 'force-dynamic'
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
export async function GET(
|
|
12
|
+
export async function GET() {
|
|
13
|
+
ensureDaemonStarted('api/connectors:get')
|
|
12
14
|
const connectors = loadConnectors()
|
|
13
15
|
// Merge runtime status from manager
|
|
14
16
|
try {
|
|
15
17
|
const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
|
|
16
18
|
for (const c of Object.values(connectors) as Connector[]) {
|
|
17
|
-
|
|
19
|
+
const runtimeStatus = getConnectorStatus(c.id)
|
|
20
|
+
c.status = runtimeStatus === 'running'
|
|
21
|
+
? 'running'
|
|
22
|
+
: c.lastError
|
|
23
|
+
? 'error'
|
|
24
|
+
: 'stopped'
|
|
18
25
|
if (c.platform === 'whatsapp') {
|
|
19
26
|
c.authenticated = isConnectorAuthenticated(c.id)
|
|
20
27
|
c.hasCredentials = hasConnectorCredentials(c.id)
|
|
@@ -28,6 +35,7 @@ export async function GET(_req: Request) {
|
|
|
28
35
|
ext.reconnectAttempts = rState.attempts
|
|
29
36
|
ext.nextRetryAt = rState.nextRetryAt
|
|
30
37
|
ext.reconnectError = rState.error
|
|
38
|
+
ext.reconnectExhausted = rState.exhausted
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
} catch { /* manager not loaded yet */ }
|
|
@@ -35,6 +43,7 @@ export async function GET(_req: Request) {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export async function POST(req: Request) {
|
|
46
|
+
ensureDaemonStarted('api/connectors:post')
|
|
38
47
|
const raw = await req.json()
|
|
39
48
|
const parsed = ConnectorCreateSchema.safeParse(raw)
|
|
40
49
|
if (!parsed.success) {
|
|
@@ -72,13 +81,8 @@ export async function POST(req: Request) {
|
|
|
72
81
|
try {
|
|
73
82
|
const { startConnector } = await import('@/lib/server/connectors/manager')
|
|
74
83
|
await startConnector(id)
|
|
75
|
-
connector.isEnabled = true
|
|
76
|
-
connector.status = 'running'
|
|
77
|
-
connectors[id] = connector
|
|
78
|
-
saveConnectors(connectors)
|
|
79
|
-
notify('connectors')
|
|
80
84
|
} catch { /* auto-start is best-effort */ }
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
return NextResponse.json(connector)
|
|
87
|
+
return NextResponse.json(loadConnectors()[id] || connector)
|
|
84
88
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills } from '@/lib/server/storage'
|
|
2
|
+
import { loadProjects, saveProjects, deleteProject, loadAgents, saveAgents, loadTasks, saveTasks, loadSchedules, saveSchedules, loadSkills, saveSkills, loadSecrets, saveSecrets } from '@/lib/server/storage'
|
|
3
3
|
import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
4
|
+
import { ensureProjectWorkspace, normalizeProjectPatchInput } from '@/lib/server/project-utils'
|
|
4
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
6
|
|
|
6
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -17,12 +18,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
17
18
|
const { id } = await params
|
|
18
19
|
const body = await req.json()
|
|
19
20
|
const result = mutateItem(ops, id, (project) => {
|
|
20
|
-
|
|
21
|
+
const patch = normalizeProjectPatchInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
|
|
22
|
+
Object.assign(project, patch, { updatedAt: Date.now() })
|
|
21
23
|
delete (project as Record<string, unknown>).id
|
|
22
24
|
project.id = id
|
|
23
25
|
return project
|
|
24
26
|
})
|
|
25
27
|
if (!result) return notFound()
|
|
28
|
+
ensureProjectWorkspace(id, result.name)
|
|
26
29
|
return NextResponse.json(result)
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -50,6 +53,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
50
53
|
clearProjectId(loadTasks, saveTasks, 'tasks')
|
|
51
54
|
clearProjectId(loadSchedules, saveSchedules, 'schedules')
|
|
52
55
|
clearProjectId(loadSkills, saveSkills, 'skills')
|
|
56
|
+
clearProjectId(loadSecrets, saveSecrets, 'secrets')
|
|
53
57
|
|
|
54
58
|
return NextResponse.json({ ok: true })
|
|
55
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { loadProjects, saveProjects } from '@/lib/server/storage'
|
|
4
|
+
import { ensureProjectWorkspace, normalizeProjectCreateInput } from '@/lib/server/project-utils'
|
|
4
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
6
|
export const dynamic = 'force-dynamic'
|
|
6
7
|
|
|
@@ -13,15 +14,15 @@ export async function POST(req: Request) {
|
|
|
13
14
|
const id = genId()
|
|
14
15
|
const now = Date.now()
|
|
15
16
|
const projects = loadProjects()
|
|
17
|
+
const normalized = normalizeProjectCreateInput(body && typeof body === 'object' ? body as Record<string, unknown> : {})
|
|
16
18
|
projects[id] = {
|
|
17
19
|
id,
|
|
18
|
-
|
|
19
|
-
description: body.description || '',
|
|
20
|
-
color: body.color || undefined,
|
|
20
|
+
...normalized,
|
|
21
21
|
createdAt: now,
|
|
22
22
|
updatedAt: now,
|
|
23
23
|
}
|
|
24
24
|
saveProjects(projects)
|
|
25
|
+
ensureProjectWorkspace(id, projects[id].name)
|
|
25
26
|
notify('projects')
|
|
26
27
|
return NextResponse.json(projects[id])
|
|
27
28
|
}
|
|
@@ -19,6 +19,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
19
19
|
if (body.service !== undefined) secret.service = body.service
|
|
20
20
|
if (body.scope !== undefined) secret.scope = body.scope
|
|
21
21
|
if (body.agentIds !== undefined) secret.agentIds = body.agentIds
|
|
22
|
+
if (body.projectId !== undefined) secret.projectId = body.projectId || undefined
|
|
22
23
|
secret.updatedAt = Date.now()
|
|
23
24
|
return secret
|
|
24
25
|
})
|
|
@@ -10,7 +10,7 @@ export async function GET(_req: Request) {
|
|
|
10
10
|
const safe = Object.fromEntries(
|
|
11
11
|
Object.entries(secrets).map(([id, s]: [string, any]) => [
|
|
12
12
|
id,
|
|
13
|
-
{ id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, createdAt: s.createdAt, updatedAt: s.updatedAt },
|
|
13
|
+
{ id: s.id, name: s.name, service: s.service, scope: s.scope, agentIds: s.agentIds, projectId: s.projectId, createdAt: s.createdAt, updatedAt: s.updatedAt },
|
|
14
14
|
])
|
|
15
15
|
)
|
|
16
16
|
return NextResponse.json(safe)
|
|
@@ -33,6 +33,7 @@ export async function POST(req: Request) {
|
|
|
33
33
|
encryptedValue: encryptKey(body.value),
|
|
34
34
|
scope: body.scope || 'global',
|
|
35
35
|
agentIds: body.agentIds || [],
|
|
36
|
+
projectId: typeof body.projectId === 'string' && body.projectId.trim() ? body.projectId.trim() : undefined,
|
|
36
37
|
createdAt: now,
|
|
37
38
|
updatedAt: now,
|
|
38
39
|
}
|
|
@@ -126,6 +126,8 @@ export async function PUT(req: Request) {
|
|
|
126
126
|
settings.taskQualityGateRequireVerification = parseBoolSetting(settings.taskQualityGateRequireVerification, false)
|
|
127
127
|
settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
|
|
128
128
|
settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
|
|
129
|
+
settings.taskManagementEnabled = parseBoolSetting(settings.taskManagementEnabled, true)
|
|
130
|
+
settings.projectManagementEnabled = parseBoolSetting(settings.projectManagementEnabled, true)
|
|
129
131
|
settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
|
|
130
132
|
settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
|
|
131
133
|
settings.sessionIdleTimeoutSec = parseIntSetting(
|
|
@@ -20,6 +20,38 @@ import { SoulLibraryPicker } from './soul-library-picker'
|
|
|
20
20
|
import { HintTip } from '@/components/shared/hint-tip'
|
|
21
21
|
|
|
22
22
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
23
|
+
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
24
|
+
|
|
25
|
+
type AgentSheetSectionId = 'overview' | 'instructions' | 'model' | 'tools'
|
|
26
|
+
|
|
27
|
+
function SectionCard({
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
action,
|
|
31
|
+
children,
|
|
32
|
+
className = '',
|
|
33
|
+
}: {
|
|
34
|
+
title: string
|
|
35
|
+
description?: string
|
|
36
|
+
action?: React.ReactNode
|
|
37
|
+
children: React.ReactNode
|
|
38
|
+
className?: string
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<section className={`mb-8 rounded-[20px] border border-white/[0.06] bg-surface/70 p-5 sm:p-6 ${className}`}>
|
|
42
|
+
<div className="mb-5 flex items-start justify-between gap-4">
|
|
43
|
+
<div>
|
|
44
|
+
<h3 className="font-display text-[17px] font-700 tracking-[-0.02em] text-text">{title}</h3>
|
|
45
|
+
{description && (
|
|
46
|
+
<p className="mt-1 text-[13px] leading-[1.6] text-text-3/75">{description}</p>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
{action}
|
|
50
|
+
</div>
|
|
51
|
+
{children}
|
|
52
|
+
</section>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
23
55
|
|
|
24
56
|
function formatHbDuration(sec: number): string {
|
|
25
57
|
if (sec >= 3600) {
|
|
@@ -106,6 +138,7 @@ export function AgentSheet() {
|
|
|
106
138
|
const credentials = useAppStore((s) => s.credentials)
|
|
107
139
|
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
108
140
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
141
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
109
142
|
const dynamicSkills = useAppStore((s) => s.skills)
|
|
110
143
|
const mcpServers = useAppStore((s) => s.mcpServers)
|
|
111
144
|
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
@@ -196,6 +229,12 @@ export function AgentSheet() {
|
|
|
196
229
|
const [soulLibraryOpen, setSoulLibraryOpen] = useState(false)
|
|
197
230
|
const promptFileRef = useRef<HTMLInputElement>(null)
|
|
198
231
|
const importFileRef = useRef<HTMLInputElement>(null)
|
|
232
|
+
const sectionRefs = useRef<Record<AgentSheetSectionId, HTMLDivElement | null>>({
|
|
233
|
+
overview: null,
|
|
234
|
+
instructions: null,
|
|
235
|
+
model: null,
|
|
236
|
+
tools: null,
|
|
237
|
+
})
|
|
199
238
|
|
|
200
239
|
const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
201
240
|
const file = e.target.files?.[0]
|
|
@@ -212,6 +251,19 @@ export function AgentSheet() {
|
|
|
212
251
|
const openclawGatewayProfiles = gatewayProfiles.filter((item) => item.provider === 'openclaw')
|
|
213
252
|
const editing = editingId ? agents[editingId] : null
|
|
214
253
|
const hasNativeCapabilities = NATIVE_CAPABILITY_PROVIDER_IDS.has(provider)
|
|
254
|
+
const globalVoiceId = typeof appSettings.elevenLabsVoiceId === 'string' ? appSettings.elevenLabsVoiceId.trim() : ''
|
|
255
|
+
const agentVoiceId = voiceId.trim()
|
|
256
|
+
const elevenLabsConfigured = appSettings.elevenLabsApiKeyConfigured === true
|
|
257
|
+
const voiceControlsAvailable = elevenLabsConfigured || appSettings.elevenLabsEnabled === true || !!globalVoiceId || !!agentVoiceId
|
|
258
|
+
const voicePlaybackEnabled = appSettings.elevenLabsEnabled === true
|
|
259
|
+
const effectiveVoiceId = agentVoiceId || globalVoiceId || FALLBACK_ELEVENLABS_VOICE_ID
|
|
260
|
+
const effectiveVoiceSource = agentVoiceId
|
|
261
|
+
? 'Agent override'
|
|
262
|
+
: globalVoiceId
|
|
263
|
+
? 'Global default'
|
|
264
|
+
: 'Built-in fallback'
|
|
265
|
+
const providerSummary = openclawEnabled ? 'OpenClaw gateway' : (currentProvider?.name || provider)
|
|
266
|
+
const modelSummary = openclawEnabled ? (gatewayProfileId ? 'Gateway-managed' : 'default') : (model || 'Select a model')
|
|
215
267
|
|
|
216
268
|
const providerNeedsKey = !editing && (
|
|
217
269
|
(currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
|
|
@@ -220,6 +272,7 @@ export function AgentSheet() {
|
|
|
220
272
|
|
|
221
273
|
useEffect(() => {
|
|
222
274
|
if (open) {
|
|
275
|
+
loadSettings()
|
|
223
276
|
loadProviders()
|
|
224
277
|
loadGatewayProfiles()
|
|
225
278
|
loadCredentials()
|
|
@@ -412,6 +465,10 @@ export function AgentSheet() {
|
|
|
412
465
|
setEditingId(null)
|
|
413
466
|
}
|
|
414
467
|
|
|
468
|
+
const jumpToSection = (sectionId: AgentSheetSectionId) => {
|
|
469
|
+
sectionRefs.current[sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
470
|
+
}
|
|
471
|
+
|
|
415
472
|
const applyGatewayProfileSelection = (nextGatewayProfileId: string | null) => {
|
|
416
473
|
setGatewayProfileId(nextGatewayProfileId)
|
|
417
474
|
const gateway = openclawGatewayProfiles.find((item) => item.id === nextGatewayProfileId)
|
|
@@ -578,6 +635,7 @@ export function AgentSheet() {
|
|
|
578
635
|
tools: editing.tools,
|
|
579
636
|
plugins: editing.plugins,
|
|
580
637
|
capabilities: editing.capabilities,
|
|
638
|
+
elevenLabsVoiceId: editing.elevenLabsVoiceId || null,
|
|
581
639
|
soul: editing.soul,
|
|
582
640
|
systemPrompt: editing.systemPrompt,
|
|
583
641
|
}],
|
|
@@ -605,11 +663,12 @@ export function AgentSheet() {
|
|
|
605
663
|
if (!importedAgent || typeof importedAgent !== 'object') throw new Error('Invalid agent pack')
|
|
606
664
|
// Strip IDs and timestamps
|
|
607
665
|
const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = importedAgent
|
|
666
|
+
void [_id, _ca, _ua, _ts]
|
|
608
667
|
await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
|
|
609
668
|
await loadAgents()
|
|
610
669
|
toast.success(data?.kind === 'swarmclaw-agent-pack' ? 'Agent pack imported' : 'Agent imported')
|
|
611
670
|
onClose()
|
|
612
|
-
} catch
|
|
671
|
+
} catch {
|
|
613
672
|
toast.error('Invalid agent JSON file')
|
|
614
673
|
}
|
|
615
674
|
}
|
|
@@ -720,6 +779,56 @@ export function AgentSheet() {
|
|
|
720
779
|
</div>
|
|
721
780
|
</div>
|
|
722
781
|
|
|
782
|
+
<div className="mb-8 rounded-[20px] border border-white/[0.06] bg-white/[0.03] p-4 sm:p-5">
|
|
783
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
|
784
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-black/10 p-3">
|
|
785
|
+
<p className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3">Provider</p>
|
|
786
|
+
<p className="mt-1 text-[14px] font-600 text-text">{providerSummary}</p>
|
|
787
|
+
</div>
|
|
788
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-black/10 p-3">
|
|
789
|
+
<p className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3">Model</p>
|
|
790
|
+
<p className="mt-1 text-[14px] font-600 text-text">{modelSummary}</p>
|
|
791
|
+
</div>
|
|
792
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-black/10 p-3">
|
|
793
|
+
<p className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3">Voice</p>
|
|
794
|
+
<p className="mt-1 text-[14px] font-600 text-text">{voiceControlsAvailable ? effectiveVoiceSource : 'Not configured'}</p>
|
|
795
|
+
{voiceControlsAvailable && (
|
|
796
|
+
<p className="mt-1 truncate text-[12px] text-text-3/75">{effectiveVoiceId}</p>
|
|
797
|
+
)}
|
|
798
|
+
</div>
|
|
799
|
+
<div className="rounded-[14px] border border-white/[0.05] bg-black/10 p-3">
|
|
800
|
+
<p className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3">Mode</p>
|
|
801
|
+
<p className="mt-1 text-[14px] font-600 text-text">{canDelegateToAgents ? 'Delegating agent' : 'Solo agent'}</p>
|
|
802
|
+
<p className="mt-1 text-[12px] text-text-3/75">
|
|
803
|
+
{routingTargets.length > 0 ? `${routingTargets.length} route${routingTargets.length === 1 ? '' : 's'} configured` : 'Single primary route'}
|
|
804
|
+
</p>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
808
|
+
{([
|
|
809
|
+
['overview', 'Overview'],
|
|
810
|
+
['instructions', 'Instructions'],
|
|
811
|
+
['model', 'Model Setup'],
|
|
812
|
+
['tools', 'Tools'],
|
|
813
|
+
] as const).map(([sectionId, label]) => (
|
|
814
|
+
<button
|
|
815
|
+
key={sectionId}
|
|
816
|
+
type="button"
|
|
817
|
+
onClick={() => jumpToSection(sectionId)}
|
|
818
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-2 text-[12px] font-600 text-text-3 transition-all hover:bg-white/[0.04] hover:text-text-2"
|
|
819
|
+
style={{ fontFamily: 'inherit' }}
|
|
820
|
+
>
|
|
821
|
+
{label}
|
|
822
|
+
</button>
|
|
823
|
+
))}
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<div ref={(node) => { sectionRefs.current.overview = node }}>
|
|
828
|
+
<SectionCard
|
|
829
|
+
title="Overview"
|
|
830
|
+
description="Basic identity, defaults, voice, heartbeat, and budget controls for this agent."
|
|
831
|
+
>
|
|
723
832
|
<div className="mb-8">
|
|
724
833
|
<SectionLabel>Name</SectionLabel>
|
|
725
834
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
@@ -756,7 +865,7 @@ export function AgentSheet() {
|
|
|
756
865
|
setAvatarSeed('')
|
|
757
866
|
toast.success('Avatar image uploaded')
|
|
758
867
|
}
|
|
759
|
-
} catch
|
|
868
|
+
} catch {
|
|
760
869
|
toast.error('Failed to upload image')
|
|
761
870
|
} finally {
|
|
762
871
|
setUploading(false)
|
|
@@ -935,20 +1044,58 @@ export function AgentSheet() {
|
|
|
935
1044
|
</div>
|
|
936
1045
|
|
|
937
1046
|
{/* ElevenLabs Voice ID */}
|
|
938
|
-
{
|
|
1047
|
+
{voiceControlsAvailable && (
|
|
939
1048
|
<div className="mb-8">
|
|
940
|
-
<
|
|
941
|
-
|
|
942
|
-
|
|
1049
|
+
<div className="mb-3 flex flex-wrap items-start justify-between gap-3">
|
|
1050
|
+
<div>
|
|
1051
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
1052
|
+
Voice & Audio <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
1053
|
+
</label>
|
|
1054
|
+
<p className="mt-1 text-[12px] leading-[1.6] text-text-3/70">
|
|
1055
|
+
Set an agent-specific ElevenLabs voice or inherit the global default configured in Settings.
|
|
1056
|
+
</p>
|
|
1057
|
+
</div>
|
|
1058
|
+
<div className={`rounded-[12px] border px-3 py-2 text-right ${
|
|
1059
|
+
agentVoiceId
|
|
1060
|
+
? 'border-accent-bright/25 bg-accent-soft/20'
|
|
1061
|
+
: 'border-white/[0.06] bg-white/[0.03]'
|
|
1062
|
+
}`}>
|
|
1063
|
+
<p className="text-[10px] font-700 uppercase tracking-[0.08em] text-text-3">
|
|
1064
|
+
{effectiveVoiceSource}
|
|
1065
|
+
</p>
|
|
1066
|
+
<p className="mt-1 max-w-[240px] truncate font-mono text-[12px] text-text-2">{effectiveVoiceId}</p>
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
943
1069
|
<input
|
|
944
1070
|
type="text"
|
|
945
1071
|
value={voiceId}
|
|
946
1072
|
onChange={(e) => setVoiceId(e.target.value)}
|
|
947
|
-
placeholder=
|
|
1073
|
+
placeholder={globalVoiceId ? `Leave blank to use ${globalVoiceId}` : 'Leave blank for the global default'}
|
|
948
1074
|
className={inputClass}
|
|
949
1075
|
style={{ fontFamily: 'inherit' }}
|
|
950
1076
|
/>
|
|
951
|
-
<
|
|
1077
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
1078
|
+
{agentVoiceId && (
|
|
1079
|
+
<button
|
|
1080
|
+
type="button"
|
|
1081
|
+
onClick={() => setVoiceId('')}
|
|
1082
|
+
className="rounded-[9px] border border-white/[0.08] bg-transparent px-2.5 py-1.5 text-[11px] font-600 text-text-3 transition-all hover:bg-white/[0.04] hover:text-text-2"
|
|
1083
|
+
style={{ fontFamily: 'inherit' }}
|
|
1084
|
+
>
|
|
1085
|
+
Use global default
|
|
1086
|
+
</button>
|
|
1087
|
+
)}
|
|
1088
|
+
{!voicePlaybackEnabled && (
|
|
1089
|
+
<span className="rounded-[9px] border border-amber-400/20 bg-amber-400/[0.08] px-2.5 py-1.5 text-[11px] font-600 text-amber-300">
|
|
1090
|
+
Voice playback is disabled globally
|
|
1091
|
+
</span>
|
|
1092
|
+
)}
|
|
1093
|
+
</div>
|
|
1094
|
+
<p className="text-[11px] text-text-3/70 mt-2">
|
|
1095
|
+
{globalVoiceId
|
|
1096
|
+
? `Global default: ${globalVoiceId}. This agent can override it with a different voice ID.`
|
|
1097
|
+
: 'No global default voice ID is set yet. If left blank, the built-in ElevenLabs fallback will be used.'}
|
|
1098
|
+
</p>
|
|
952
1099
|
</div>
|
|
953
1100
|
)}
|
|
954
1101
|
|
|
@@ -1107,6 +1254,8 @@ export function AgentSheet() {
|
|
|
1107
1254
|
: 'When a configured cap is exceeded, a warning is shown but runs continue.'}
|
|
1108
1255
|
</p>
|
|
1109
1256
|
</div>
|
|
1257
|
+
</SectionCard>
|
|
1258
|
+
</div>
|
|
1110
1259
|
|
|
1111
1260
|
{/* Wallet Section */}
|
|
1112
1261
|
{editingId && (
|
|
@@ -1128,6 +1277,11 @@ export function AgentSheet() {
|
|
|
1128
1277
|
/>
|
|
1129
1278
|
)}
|
|
1130
1279
|
|
|
1280
|
+
<div ref={(node) => { sectionRefs.current.instructions = node }}>
|
|
1281
|
+
<SectionCard
|
|
1282
|
+
title="Instructions & Continuity"
|
|
1283
|
+
description="Define personality, system behavior, and long-running context this agent should preserve."
|
|
1284
|
+
>
|
|
1131
1285
|
{provider !== 'openclaw' && (
|
|
1132
1286
|
<div className="mb-8">
|
|
1133
1287
|
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -1322,7 +1476,14 @@ export function AgentSheet() {
|
|
|
1322
1476
|
/>
|
|
1323
1477
|
</div>
|
|
1324
1478
|
</div>
|
|
1479
|
+
</SectionCard>
|
|
1480
|
+
</div>
|
|
1325
1481
|
|
|
1482
|
+
<div ref={(node) => { sectionRefs.current.model = node }}>
|
|
1483
|
+
<SectionCard
|
|
1484
|
+
title="Model Setup"
|
|
1485
|
+
description="Choose the provider, credentials, routing, and gateway preferences this agent should use."
|
|
1486
|
+
>
|
|
1326
1487
|
{/* OpenClaw Gateway Fields */}
|
|
1327
1488
|
{openclawEnabled && (
|
|
1328
1489
|
<div className="mb-8 space-y-5">
|
|
@@ -1411,13 +1572,13 @@ export function AgentSheet() {
|
|
|
1411
1572
|
onClick={async () => {
|
|
1412
1573
|
setSavingKey(true)
|
|
1413
1574
|
try {
|
|
1414
|
-
const cred = await api<
|
|
1575
|
+
const cred = await api<{ id: string }>('POST', '/credentials', { provider: 'openclaw', name: newKeyName.trim() || 'OpenClaw token', apiKey: newKeyValue.trim() })
|
|
1415
1576
|
await loadCredentials()
|
|
1416
1577
|
setCredentialId(cred.id)
|
|
1417
1578
|
setAddingKey(false)
|
|
1418
1579
|
setNewKeyName('')
|
|
1419
1580
|
setNewKeyValue('')
|
|
1420
|
-
} catch (err:
|
|
1581
|
+
} catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
|
|
1421
1582
|
finally { setSavingKey(false) }
|
|
1422
1583
|
}}
|
|
1423
1584
|
className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40"
|
|
@@ -1670,13 +1831,13 @@ export function AgentSheet() {
|
|
|
1670
1831
|
onClick={async () => {
|
|
1671
1832
|
setSavingKey(true)
|
|
1672
1833
|
try {
|
|
1673
|
-
const cred = await api<
|
|
1834
|
+
const cred = await api<{ id: string }>('POST', '/credentials', { provider, name: newKeyName.trim() || `${provider} key`, apiKey: newKeyValue.trim() })
|
|
1674
1835
|
await loadCredentials()
|
|
1675
1836
|
setCredentialId(cred.id)
|
|
1676
1837
|
setAddingKey(false)
|
|
1677
1838
|
setNewKeyName('')
|
|
1678
1839
|
setNewKeyValue('')
|
|
1679
|
-
} catch (err:
|
|
1840
|
+
} catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
|
|
1680
1841
|
finally { setSavingKey(false) }
|
|
1681
1842
|
}}
|
|
1682
1843
|
className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40"
|
|
@@ -1877,7 +2038,14 @@ export function AgentSheet() {
|
|
|
1877
2038
|
<p className="text-[11px] text-text-3/70 mt-2">No route pool yet. Add one if this agent should switch between cheaper, stronger, or gateway-specific models.</p>
|
|
1878
2039
|
)}
|
|
1879
2040
|
</div>
|
|
2041
|
+
</SectionCard>
|
|
2042
|
+
</div>
|
|
1880
2043
|
|
|
2044
|
+
<div ref={(node) => { sectionRefs.current.tools = node }}>
|
|
2045
|
+
<SectionCard
|
|
2046
|
+
title="Tools & Delegation"
|
|
2047
|
+
description="Enable plugins, skills, MCP tools, and delegation behavior for this agent."
|
|
2048
|
+
>
|
|
1881
2049
|
{/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
|
|
1882
2050
|
{!hasNativeCapabilities && (
|
|
1883
2051
|
<div className="mb-8">
|
|
@@ -2024,7 +2192,7 @@ export function AgentSheet() {
|
|
|
2024
2192
|
</label>
|
|
2025
2193
|
<p className="text-[12px] text-text-3/60 mb-3">Connect external tool servers to this agent via MCP.</p>
|
|
2026
2194
|
<div className="flex flex-wrap gap-2">
|
|
2027
|
-
{Object.values(mcpServers).map((s
|
|
2195
|
+
{Object.values(mcpServers).map((s) => {
|
|
2028
2196
|
const active = mcpServerIds.includes(s.id)
|
|
2029
2197
|
return (
|
|
2030
2198
|
<button
|
|
@@ -2056,7 +2224,7 @@ export function AgentSheet() {
|
|
|
2056
2224
|
</p>
|
|
2057
2225
|
<div className="space-y-4">
|
|
2058
2226
|
{mcpServerIds.map((serverId) => {
|
|
2059
|
-
const server =
|
|
2227
|
+
const server = mcpServers[serverId]
|
|
2060
2228
|
const serverTools = mcpTools[serverId]
|
|
2061
2229
|
if (!server || !serverTools?.length) return null
|
|
2062
2230
|
const safeName = server.name.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
@@ -2121,6 +2289,8 @@ export function AgentSheet() {
|
|
|
2121
2289
|
/>
|
|
2122
2290
|
</div>
|
|
2123
2291
|
)}
|
|
2292
|
+
</SectionCard>
|
|
2293
|
+
</div>
|
|
2124
2294
|
|
|
2125
2295
|
{/* Provider key warning */}
|
|
2126
2296
|
{providerNeedsKey && (
|