@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.
Files changed (63) hide show
  1. package/README.md +10 -9
  2. package/package.json +1 -1
  3. package/src/app/api/chats/route.ts +1 -0
  4. package/src/app/api/connectors/[id]/route.ts +20 -2
  5. package/src/app/api/connectors/route.ts +12 -8
  6. package/src/app/api/projects/[id]/route.ts +6 -2
  7. package/src/app/api/projects/route.ts +4 -3
  8. package/src/app/api/secrets/[id]/route.ts +1 -0
  9. package/src/app/api/secrets/route.ts +2 -1
  10. package/src/app/api/settings/route.ts +2 -0
  11. package/src/components/agents/agent-sheet.tsx +184 -14
  12. package/src/components/chat/chat-area.tsx +36 -19
  13. package/src/components/chat/chat-header.tsx +4 -0
  14. package/src/components/chat/delegation-banner.test.ts +14 -1
  15. package/src/components/chat/delegation-banner.tsx +1 -1
  16. package/src/components/layout/app-layout.tsx +40 -23
  17. package/src/components/projects/project-detail.tsx +217 -0
  18. package/src/components/projects/project-sheet.tsx +176 -4
  19. package/src/components/shared/settings/section-capability-policy.tsx +38 -0
  20. package/src/components/shared/settings/section-voice.tsx +11 -3
  21. package/src/components/tasks/approvals-panel.tsx +177 -18
  22. package/src/components/tasks/task-board.tsx +137 -23
  23. package/src/components/tasks/task-card.tsx +29 -0
  24. package/src/components/tasks/task-sheet.tsx +16 -4
  25. package/src/lib/server/capability-router.test.ts +22 -0
  26. package/src/lib/server/capability-router.ts +54 -18
  27. package/src/lib/server/chat-execution.ts +25 -1
  28. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  29. package/src/lib/server/connectors/manager.ts +99 -74
  30. package/src/lib/server/daemon-state.ts +83 -46
  31. package/src/lib/server/elevenlabs.test.ts +59 -1
  32. package/src/lib/server/heartbeat-service.ts +5 -1
  33. package/src/lib/server/main-agent-loop.test.ts +260 -0
  34. package/src/lib/server/main-agent-loop.ts +559 -14
  35. package/src/lib/server/orchestrator-lg.ts +1 -0
  36. package/src/lib/server/orchestrator.ts +2 -0
  37. package/src/lib/server/plugins.ts +6 -1
  38. package/src/lib/server/project-context.ts +162 -0
  39. package/src/lib/server/project-utils.ts +150 -0
  40. package/src/lib/server/queue-followups.test.ts +147 -2
  41. package/src/lib/server/queue.ts +234 -7
  42. package/src/lib/server/session-run-manager.ts +31 -0
  43. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  44. package/src/lib/server/session-tools/connector.ts +26 -1
  45. package/src/lib/server/session-tools/context.ts +5 -0
  46. package/src/lib/server/session-tools/crud.ts +265 -76
  47. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  48. package/src/lib/server/session-tools/delegate.ts +38 -2
  49. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  50. package/src/lib/server/session-tools/memory.ts +14 -2
  51. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  52. package/src/lib/server/session-tools/platform.ts +60 -19
  53. package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
  54. package/src/lib/server/session-tools/web.ts +153 -6
  55. package/src/lib/server/stream-agent-chat.test.ts +27 -2
  56. package/src/lib/server/stream-agent-chat.ts +104 -30
  57. package/src/lib/server/tool-aliases.ts +2 -0
  58. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  59. package/src/lib/server/tool-capability-policy.ts +29 -1
  60. package/src/lib/server/tool-planning.test.ts +44 -0
  61. package/src/lib/server/tool-planning.ts +269 -0
  62. package/src/lib/tool-definitions.ts +2 -1
  63. 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.7 curl ... | bash`
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.7 style)
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.7 Release Readiness Notes
714
+ #### v0.7.8 Release Readiness Notes
715
715
 
716
- Before shipping `v0.7.7`, confirm the following user-facing changes are reflected in docs:
716
+ Before shipping `v0.7.8`, confirm the following user-facing changes are reflected in docs:
717
717
 
718
- 1. OpenClaw docs cover Smart Deploy end-to-end: onboarding, Providers, gateway editor, official-only SSH/VPS flows, safe exposure presets, and restore/backup lifecycle controls.
719
- 2. Agent and provider docs explain gateway routing by tags/use-case, richer external runtime visibility, and import/export/clone flows for saved gateways.
720
- 3. Site and README install/version strings are updated to `v0.7.7`, including install snippets, release notes index text, and sidebar/footer labels.
721
- 4. Release notes summarize the user-visible operator changes from the current worktree, especially SSH deploy, remote lifecycle controls, routing preferences, and onboarding persistence.
722
- 5. CLI docs include the expanded `openclaw deploy-*`, `openclaw remote-*`, and verify surfaces and do not reference removed or unofficial deployment paths.
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.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": {
@@ -101,6 +101,7 @@ export async function POST(req: Request) {
101
101
  claudeCode: null,
102
102
  codex: null,
103
103
  opencode: null,
104
+ gemini: null,
104
105
  },
105
106
  messages: Array.isArray(body.messages) ? body.messages : [],
106
107
  createdAt: Date.now(), lastActiveAt: Date.now(),
@@ -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
- connector.status = getConnectorStatus(id)
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(_req: Request) {
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
- c.status = getConnectorStatus(c.id)
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
- Object.assign(project, body, { updatedAt: Date.now() })
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
- name: body.name || 'Unnamed Project',
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 (err) {
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 (err: unknown) {
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
- {appSettings.elevenLabsEnabled && (
1047
+ {voiceControlsAvailable && (
939
1048
  <div className="mb-8">
940
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
941
- ElevenLabs Voice ID <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
942
- </label>
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="Leave blank for global default"
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
- <p className="text-[11px] text-text-3/70 mt-1.5">Override the default voice for this agent. Leave blank to use the global default.</p>
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<any>('POST', '/credentials', { provider: 'openclaw', name: newKeyName.trim() || 'OpenClaw token', apiKey: newKeyValue.trim() })
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: any) { toast.error(`Failed to save: ${err.message}`) }
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<any>('POST', '/credentials', { provider, name: newKeyName.trim() || `${provider} key`, apiKey: newKeyValue.trim() })
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: any) { toast.error(`Failed to save: ${err.message}`) }
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: any) => {
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 = (mcpServers as Record<string, any>)[serverId]
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 && (