@swarmclawai/swarmclaw 0.7.6 → 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 (86) hide show
  1. package/README.md +19 -10
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +16 -0
  4. package/src/app/api/agents/route.ts +2 -0
  5. package/src/app/api/chats/[id]/route.ts +21 -1
  6. package/src/app/api/chats/route.ts +13 -1
  7. package/src/app/api/connectors/[id]/route.ts +20 -2
  8. package/src/app/api/connectors/route.ts +12 -8
  9. package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
  10. package/src/app/api/external-agents/[id]/route.ts +38 -6
  11. package/src/app/api/external-agents/route.ts +17 -1
  12. package/src/app/api/gateways/[id]/health/route.ts +8 -0
  13. package/src/app/api/gateways/[id]/route.ts +53 -1
  14. package/src/app/api/gateways/route.ts +53 -0
  15. package/src/app/api/openclaw/deploy/route.ts +139 -0
  16. package/src/app/api/projects/[id]/route.ts +6 -2
  17. package/src/app/api/projects/route.ts +4 -3
  18. package/src/app/api/secrets/[id]/route.ts +1 -0
  19. package/src/app/api/secrets/route.ts +2 -1
  20. package/src/app/api/settings/route.ts +2 -0
  21. package/src/cli/index.js +40 -0
  22. package/src/cli/index.test.js +68 -0
  23. package/src/cli/spec.js +60 -0
  24. package/src/components/agents/agent-sheet.tsx +281 -33
  25. package/src/components/auth/setup-wizard.tsx +75 -2
  26. package/src/components/chat/chat-area.tsx +36 -19
  27. package/src/components/chat/chat-header.tsx +4 -0
  28. package/src/components/chat/delegation-banner.test.ts +14 -1
  29. package/src/components/chat/delegation-banner.tsx +1 -1
  30. package/src/components/gateways/gateway-sheet.tsx +140 -8
  31. package/src/components/layout/app-layout.tsx +40 -23
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
  33. package/src/components/projects/project-detail.tsx +217 -0
  34. package/src/components/projects/project-sheet.tsx +176 -4
  35. package/src/components/providers/provider-list.tsx +221 -17
  36. package/src/components/shared/settings/section-capability-policy.tsx +38 -0
  37. package/src/components/shared/settings/section-voice.tsx +11 -3
  38. package/src/components/tasks/approvals-panel.tsx +177 -18
  39. package/src/components/tasks/task-board.tsx +137 -23
  40. package/src/components/tasks/task-card.tsx +29 -0
  41. package/src/components/tasks/task-sheet.tsx +16 -4
  42. package/src/lib/server/agent-runtime-config.ts +142 -7
  43. package/src/lib/server/agent-thread-session.ts +9 -1
  44. package/src/lib/server/capability-router.test.ts +22 -0
  45. package/src/lib/server/capability-router.ts +54 -18
  46. package/src/lib/server/chat-execution.ts +33 -3
  47. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  48. package/src/lib/server/connectors/manager.ts +99 -74
  49. package/src/lib/server/daemon-state.ts +83 -46
  50. package/src/lib/server/elevenlabs.test.ts +59 -1
  51. package/src/lib/server/heartbeat-service.ts +5 -1
  52. package/src/lib/server/main-agent-loop.test.ts +260 -0
  53. package/src/lib/server/main-agent-loop.ts +559 -14
  54. package/src/lib/server/openclaw-deploy.test.ts +8 -0
  55. package/src/lib/server/openclaw-deploy.ts +679 -19
  56. package/src/lib/server/orchestrator-lg.ts +1 -0
  57. package/src/lib/server/orchestrator.ts +11 -0
  58. package/src/lib/server/plugins.ts +6 -1
  59. package/src/lib/server/project-context.ts +162 -0
  60. package/src/lib/server/project-utils.ts +150 -0
  61. package/src/lib/server/queue-followups.test.ts +147 -2
  62. package/src/lib/server/queue.ts +278 -8
  63. package/src/lib/server/session-run-manager.ts +31 -0
  64. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  65. package/src/lib/server/session-tools/connector.ts +26 -1
  66. package/src/lib/server/session-tools/context.ts +5 -0
  67. package/src/lib/server/session-tools/crud.ts +265 -76
  68. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  69. package/src/lib/server/session-tools/delegate.ts +38 -2
  70. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  71. package/src/lib/server/session-tools/memory.ts +14 -2
  72. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  73. package/src/lib/server/session-tools/platform.ts +60 -19
  74. package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
  75. package/src/lib/server/session-tools/web.ts +153 -6
  76. package/src/lib/server/stream-agent-chat.test.ts +27 -2
  77. package/src/lib/server/stream-agent-chat.ts +104 -30
  78. package/src/lib/server/tool-aliases.ts +2 -0
  79. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  80. package/src/lib/server/tool-capability-policy.ts +29 -1
  81. package/src/lib/server/tool-planning.test.ts +44 -0
  82. package/src/lib/server/tool-planning.ts +269 -0
  83. package/src/lib/setup-defaults.ts +2 -2
  84. package/src/lib/tool-definitions.ts +2 -1
  85. package/src/lib/validation/schemas.ts +9 -0
  86. package/src/types/index.ts +104 -0
@@ -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) {
@@ -72,6 +104,24 @@ function parseIdentityList(value: string): string[] {
72
104
  })
73
105
  }
74
106
 
107
+ function formatGatewayTagList(value: string[] | null | undefined): string {
108
+ return Array.isArray(value) ? value.join(', ') : ''
109
+ }
110
+
111
+ function parseGatewayTagList(value: string): string[] {
112
+ const seen = new Set<string>()
113
+ return value
114
+ .split(/[,\n]/)
115
+ .map((entry) => entry.trim())
116
+ .filter((entry) => {
117
+ if (!entry) return false
118
+ const key = entry.toLowerCase()
119
+ if (seen.has(key)) return false
120
+ seen.add(key)
121
+ return true
122
+ })
123
+ }
124
+
75
125
  export function AgentSheet() {
76
126
  const open = useAppStore((s) => s.agentSheetOpen)
77
127
  const setOpen = useAppStore((s) => s.setAgentSheetOpen)
@@ -88,6 +138,7 @@ export function AgentSheet() {
88
138
  const credentials = useAppStore((s) => s.credentials)
89
139
  const loadCredentials = useAppStore((s) => s.loadCredentials)
90
140
  const appSettings = useAppStore((s) => s.appSettings)
141
+ const loadSettings = useAppStore((s) => s.loadSettings)
91
142
  const dynamicSkills = useAppStore((s) => s.skills)
92
143
  const mcpServers = useAppStore((s) => s.mcpServers)
93
144
  const loadSkills = useAppStore((s) => s.loadSkills)
@@ -113,6 +164,8 @@ export function AgentSheet() {
113
164
  const [credentialId, setCredentialId] = useState<string | null>(null)
114
165
  const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
115
166
  const [gatewayProfileId, setGatewayProfileId] = useState<string | null>(null)
167
+ const [preferredGatewayTagsText, setPreferredGatewayTagsText] = useState('')
168
+ const [preferredGatewayUseCase, setPreferredGatewayUseCase] = useState('')
116
169
  const [routingStrategy, setRoutingStrategy] = useState<AgentRoutingStrategy>('single')
117
170
  const [routingTargets, setRoutingTargets] = useState<AgentRoutingTarget[]>([])
118
171
  const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
@@ -176,6 +229,12 @@ export function AgentSheet() {
176
229
  const [soulLibraryOpen, setSoulLibraryOpen] = useState(false)
177
230
  const promptFileRef = useRef<HTMLInputElement>(null)
178
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
+ })
179
238
 
180
239
  const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
181
240
  const file = e.target.files?.[0]
@@ -192,6 +251,19 @@ export function AgentSheet() {
192
251
  const openclawGatewayProfiles = gatewayProfiles.filter((item) => item.provider === 'openclaw')
193
252
  const editing = editingId ? agents[editingId] : null
194
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')
195
267
 
196
268
  const providerNeedsKey = !editing && (
197
269
  (currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
@@ -200,6 +272,7 @@ export function AgentSheet() {
200
272
 
201
273
  useEffect(() => {
202
274
  if (open) {
275
+ loadSettings()
203
276
  loadProviders()
204
277
  loadGatewayProfiles()
205
278
  loadCredentials()
@@ -220,6 +293,8 @@ export function AgentSheet() {
220
293
  setCredentialId(editing.credentialId || null)
221
294
  setApiEndpoint(editing.apiEndpoint || null)
222
295
  setGatewayProfileId(editing.gatewayProfileId || null)
296
+ setPreferredGatewayTagsText(formatGatewayTagList(editing.preferredGatewayTags))
297
+ setPreferredGatewayUseCase(editing.preferredGatewayUseCase || '')
223
298
  setRoutingStrategy(editing.routingStrategy || 'single')
224
299
  setRoutingTargets(editing.routingTargets || [])
225
300
  setPlatformAssignScope(editing.platformAssignScope || 'self')
@@ -287,6 +362,8 @@ export function AgentSheet() {
287
362
  setCredentialId(null)
288
363
  setApiEndpoint(null)
289
364
  setGatewayProfileId(null)
365
+ setPreferredGatewayTagsText('')
366
+ setPreferredGatewayUseCase('')
290
367
  setRoutingStrategy('single')
291
368
  setRoutingTargets([])
292
369
  setPlatformAssignScope('self')
@@ -388,6 +465,10 @@ export function AgentSheet() {
388
465
  setEditingId(null)
389
466
  }
390
467
 
468
+ const jumpToSection = (sectionId: AgentSheetSectionId) => {
469
+ sectionRefs.current[sectionId]?.scrollIntoView({ behavior: 'smooth', block: 'start' })
470
+ }
471
+
391
472
  const applyGatewayProfileSelection = (nextGatewayProfileId: string | null) => {
392
473
  setGatewayProfileId(nextGatewayProfileId)
393
474
  const gateway = openclawGatewayProfiles.find((item) => item.id === nextGatewayProfileId)
@@ -422,6 +503,8 @@ export function AgentSheet() {
422
503
  fallbackCredentialIds,
423
504
  apiEndpoint,
424
505
  gatewayProfileId,
506
+ preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
507
+ preferredGatewayUseCase: preferredGatewayUseCase || null,
425
508
  priority: routingTargets.length + 1,
426
509
  }
427
510
  setRoutingTargets((current) => [...current, nextTarget])
@@ -463,9 +546,13 @@ export function AgentSheet() {
463
546
  credentialId,
464
547
  apiEndpoint: normalizedEndpoint,
465
548
  gatewayProfileId,
549
+ preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
550
+ preferredGatewayUseCase: preferredGatewayUseCase || null,
466
551
  routingStrategy,
467
552
  routingTargets: routingTargets.map((target, index) => ({
468
553
  ...target,
554
+ preferredGatewayTags: parseGatewayTagList(formatGatewayTagList(target.preferredGatewayTags)),
555
+ preferredGatewayUseCase: target.preferredGatewayUseCase || null,
469
556
  priority: typeof target.priority === 'number' ? target.priority : index + 1,
470
557
  })),
471
558
  subAgentIds: canDelegateToAgents ? subAgentIds : [],
@@ -548,6 +635,7 @@ export function AgentSheet() {
548
635
  tools: editing.tools,
549
636
  plugins: editing.plugins,
550
637
  capabilities: editing.capabilities,
638
+ elevenLabsVoiceId: editing.elevenLabsVoiceId || null,
551
639
  soul: editing.soul,
552
640
  systemPrompt: editing.systemPrompt,
553
641
  }],
@@ -575,11 +663,12 @@ export function AgentSheet() {
575
663
  if (!importedAgent || typeof importedAgent !== 'object') throw new Error('Invalid agent pack')
576
664
  // Strip IDs and timestamps
577
665
  const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = importedAgent
666
+ void [_id, _ca, _ua, _ts]
578
667
  await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
579
668
  await loadAgents()
580
669
  toast.success(data?.kind === 'swarmclaw-agent-pack' ? 'Agent pack imported' : 'Agent imported')
581
670
  onClose()
582
- } catch (err) {
671
+ } catch {
583
672
  toast.error('Invalid agent JSON file')
584
673
  }
585
674
  }
@@ -690,6 +779,56 @@ export function AgentSheet() {
690
779
  </div>
691
780
  </div>
692
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
+ >
693
832
  <div className="mb-8">
694
833
  <SectionLabel>Name</SectionLabel>
695
834
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
@@ -726,7 +865,7 @@ export function AgentSheet() {
726
865
  setAvatarSeed('')
727
866
  toast.success('Avatar image uploaded')
728
867
  }
729
- } catch (err: unknown) {
868
+ } catch {
730
869
  toast.error('Failed to upload image')
731
870
  } finally {
732
871
  setUploading(false)
@@ -905,20 +1044,58 @@ export function AgentSheet() {
905
1044
  </div>
906
1045
 
907
1046
  {/* ElevenLabs Voice ID */}
908
- {appSettings.elevenLabsEnabled && (
1047
+ {voiceControlsAvailable && (
909
1048
  <div className="mb-8">
910
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
911
- ElevenLabs Voice ID <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
912
- </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>
913
1069
  <input
914
1070
  type="text"
915
1071
  value={voiceId}
916
1072
  onChange={(e) => setVoiceId(e.target.value)}
917
- placeholder="Leave blank for global default"
1073
+ placeholder={globalVoiceId ? `Leave blank to use ${globalVoiceId}` : 'Leave blank for the global default'}
918
1074
  className={inputClass}
919
1075
  style={{ fontFamily: 'inherit' }}
920
1076
  />
921
- <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>
922
1099
  </div>
923
1100
  )}
924
1101
 
@@ -1077,6 +1254,8 @@ export function AgentSheet() {
1077
1254
  : 'When a configured cap is exceeded, a warning is shown but runs continue.'}
1078
1255
  </p>
1079
1256
  </div>
1257
+ </SectionCard>
1258
+ </div>
1080
1259
 
1081
1260
  {/* Wallet Section */}
1082
1261
  {editingId && (
@@ -1098,6 +1277,11 @@ export function AgentSheet() {
1098
1277
  />
1099
1278
  )}
1100
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
+ >
1101
1285
  {provider !== 'openclaw' && (
1102
1286
  <div className="mb-8">
1103
1287
  <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
@@ -1292,7 +1476,14 @@ export function AgentSheet() {
1292
1476
  />
1293
1477
  </div>
1294
1478
  </div>
1479
+ </SectionCard>
1480
+ </div>
1295
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
+ >
1296
1487
  {/* OpenClaw Gateway Fields */}
1297
1488
  {openclawEnabled && (
1298
1489
  <div className="mb-8 space-y-5">
@@ -1381,13 +1572,13 @@ export function AgentSheet() {
1381
1572
  onClick={async () => {
1382
1573
  setSavingKey(true)
1383
1574
  try {
1384
- 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() })
1385
1576
  await loadCredentials()
1386
1577
  setCredentialId(cred.id)
1387
1578
  setAddingKey(false)
1388
1579
  setNewKeyName('')
1389
1580
  setNewKeyValue('')
1390
- } 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)}`) }
1391
1582
  finally { setSavingKey(false) }
1392
1583
  }}
1393
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"
@@ -1640,13 +1831,13 @@ export function AgentSheet() {
1640
1831
  onClick={async () => {
1641
1832
  setSavingKey(true)
1642
1833
  try {
1643
- 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() })
1644
1835
  await loadCredentials()
1645
1836
  setCredentialId(cred.id)
1646
1837
  setAddingKey(false)
1647
1838
  setNewKeyName('')
1648
1839
  setNewKeyValue('')
1649
- } 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)}`) }
1650
1841
  finally { setSavingKey(false) }
1651
1842
  }}
1652
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"
@@ -1698,6 +1889,34 @@ export function AgentSheet() {
1698
1889
  </div>
1699
1890
  )}
1700
1891
 
1892
+ {(provider === 'openclaw' || routingTargets.some((target) => target.provider === 'openclaw') || openclawGatewayProfiles.length > 0) && (
1893
+ <div className="mb-8">
1894
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1895
+ Gateway Preferences <HintTip text="When multiple OpenClaw gateways are available, prefer matching tags or deployment templates before falling back to the default route." />
1896
+ </label>
1897
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1898
+ <input
1899
+ type="text"
1900
+ value={preferredGatewayTagsText}
1901
+ onChange={(e) => setPreferredGatewayTagsText(e.target.value)}
1902
+ placeholder="gpu, local, research"
1903
+ className={inputClass}
1904
+ />
1905
+ <select value={preferredGatewayUseCase} onChange={(e) => setPreferredGatewayUseCase(e.target.value)} className={inputClass}>
1906
+ <option value="">Any OpenClaw template</option>
1907
+ <option value="local-dev">Local Dev</option>
1908
+ <option value="single-vps">Single VPS</option>
1909
+ <option value="private-tailnet">Private Tailnet</option>
1910
+ <option value="browser-heavy">Browser Heavy</option>
1911
+ <option value="team-control">Team Control</option>
1912
+ </select>
1913
+ </div>
1914
+ <p className="text-[11px] text-text-3/70 mt-2">
1915
+ These preferences bias scheduling toward matching OpenClaw control planes without hard-locking the agent to one gateway.
1916
+ </p>
1917
+ </div>
1918
+ )}
1919
+
1701
1920
  <div className="mb-8">
1702
1921
  <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1703
1922
  Model Routing <HintTip text="Route this agent through a provider/model pool instead of a single fixed model. The base provider remains the default when no route matches." />
@@ -1752,25 +1971,45 @@ export function AgentSheet() {
1752
1971
  />
1753
1972
  </div>
1754
1973
  {target.provider === 'openclaw' && openclawGatewayProfiles.length > 0 && (
1755
- <select
1756
- value={target.gatewayProfileId || ''}
1757
- onChange={(e) => {
1758
- const nextId = e.target.value || null
1759
- const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
1760
- updateRoutingTarget(target.id, {
1761
- gatewayProfileId: nextId,
1762
- apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
1763
- credentialId: gateway?.credentialId || target.credentialId || null,
1764
- model: target.model || 'default',
1765
- })
1766
- }}
1767
- className={inputClass}
1768
- >
1769
- <option value="">Custom OpenClaw endpoint</option>
1770
- {openclawGatewayProfiles.map((gateway) => (
1771
- <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
1772
- ))}
1773
- </select>
1974
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1975
+ <select
1976
+ value={target.gatewayProfileId || ''}
1977
+ onChange={(e) => {
1978
+ const nextId = e.target.value || null
1979
+ const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
1980
+ updateRoutingTarget(target.id, {
1981
+ gatewayProfileId: nextId,
1982
+ apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
1983
+ credentialId: gateway?.credentialId || target.credentialId || null,
1984
+ model: target.model || 'default',
1985
+ })
1986
+ }}
1987
+ className={inputClass}
1988
+ >
1989
+ <option value="">Custom OpenClaw endpoint</option>
1990
+ {openclawGatewayProfiles.map((gateway) => (
1991
+ <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
1992
+ ))}
1993
+ </select>
1994
+ <input
1995
+ value={formatGatewayTagList(target.preferredGatewayTags)}
1996
+ onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayTags: parseGatewayTagList(e.target.value) })}
1997
+ placeholder="Prefer tags"
1998
+ className={inputClass}
1999
+ />
2000
+ <select
2001
+ value={target.preferredGatewayUseCase || ''}
2002
+ onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayUseCase: e.target.value || null })}
2003
+ className={inputClass}
2004
+ >
2005
+ <option value="">Any OpenClaw template</option>
2006
+ <option value="local-dev">Local Dev</option>
2007
+ <option value="single-vps">Single VPS</option>
2008
+ <option value="private-tailnet">Private Tailnet</option>
2009
+ <option value="browser-heavy">Browser Heavy</option>
2010
+ <option value="team-control">Team Control</option>
2011
+ </select>
2012
+ </div>
1774
2013
  )}
1775
2014
  <div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
1776
2015
  <input
@@ -1799,7 +2038,14 @@ export function AgentSheet() {
1799
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>
1800
2039
  )}
1801
2040
  </div>
2041
+ </SectionCard>
2042
+ </div>
1802
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
+ >
1803
2049
  {/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
1804
2050
  {!hasNativeCapabilities && (
1805
2051
  <div className="mb-8">
@@ -1946,7 +2192,7 @@ export function AgentSheet() {
1946
2192
  </label>
1947
2193
  <p className="text-[12px] text-text-3/60 mb-3">Connect external tool servers to this agent via MCP.</p>
1948
2194
  <div className="flex flex-wrap gap-2">
1949
- {Object.values(mcpServers).map((s: any) => {
2195
+ {Object.values(mcpServers).map((s) => {
1950
2196
  const active = mcpServerIds.includes(s.id)
1951
2197
  return (
1952
2198
  <button
@@ -1978,7 +2224,7 @@ export function AgentSheet() {
1978
2224
  </p>
1979
2225
  <div className="space-y-4">
1980
2226
  {mcpServerIds.map((serverId) => {
1981
- const server = (mcpServers as Record<string, any>)[serverId]
2227
+ const server = mcpServers[serverId]
1982
2228
  const serverTools = mcpTools[serverId]
1983
2229
  if (!server || !serverTools?.length) return null
1984
2230
  const safeName = server.name.replace(/[^a-zA-Z0-9_]/g, '_')
@@ -2043,6 +2289,8 @@ export function AgentSheet() {
2043
2289
  />
2044
2290
  </div>
2045
2291
  )}
2292
+ </SectionCard>
2293
+ </div>
2046
2294
 
2047
2295
  {/* Provider key warning */}
2048
2296
  {providerNeedsKey && (