/ src / components / agents / agent-sheet.tsx
agent-sheet.tsx
   1  'use client'
   2  
   3  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
   4  import { useAppStore } from '@/stores/use-app-store'
   5  import { selectActiveSessionId } from '@/stores/slices/session-slice'
   6  import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
   7  import { api } from '@/lib/app/api-client'
   8  import { fetchProviderModelDiscovery } from '@/lib/provider-model-discovery-client'
   9  import { sleep } from '@/lib/shared-utils'
  10  import { BottomSheet } from '@/components/shared/bottom-sheet'
  11  import { toast } from 'sonner'
  12  import { ModelCombobox } from '@/components/shared/model-combobox'
  13  import type { ProviderType, ClaudeSkill, AgentPackManifest, AgentRoutingStrategy, AgentRoutingTarget } from '@/types'
  14  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
  15  import { MCP_INJECTION_PROVIDER_IDS, NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS, WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
  16  import { isOrchestratorProviderEligible } from '@/lib/orchestrator-config'
  17  import { AgentAvatar } from './agent-avatar'
  18  import { AgentPickerList } from '@/components/shared/agent-picker-list'
  19  import { randomSoul } from '@/lib/soul-suggestions'
  20  import { copyTextToClipboard } from '@/lib/clipboard'
  21  import { SectionLabel } from '@/components/shared/section-label'
  22  import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
  23  import { SoulLibraryPicker } from './soul-library-picker'
  24  import { HintTip } from '@/components/shared/hint-tip'
  25  import { StatusDot } from '@/components/ui/status-dot'
  26  import { resolveStoredOllamaMode } from '@/lib/ollama-mode'
  27  import { errorMessage } from '@/lib/shared-utils'
  28  import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
  29  import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
  30  import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
  31  import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
  32  import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
  33  
  34  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
  35  const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
  36  const AUTO_SYNC_MODEL_PROVIDER_IDS = new Set<ProviderType>([
  37    'openai',
  38    'openrouter',
  39    'anthropic',
  40    'google',
  41    'deepseek',
  42    'groq',
  43    'together',
  44    'mistral',
  45    'xai',
  46    'fireworks',
  47    'nebius',
  48    'deepinfra',
  49    'hermes',
  50    'ollama',
  51  ])
  52  const CONNECTION_TEST_TIMEOUT_MS = 40_000
  53  type AgentProviderId = string
  54  
  55  function SectionCard({
  56    title,
  57    description,
  58    action,
  59    children,
  60    className = '',
  61  }: {
  62    title: string
  63    description?: string
  64    action?: React.ReactNode
  65    children: React.ReactNode
  66    className?: string
  67  }) {
  68    return (
  69      <section className={`mb-8 rounded-[20px] border border-white/[0.06] bg-surface/70 p-5 sm:p-6 ${className}`}>
  70        <div className="mb-5 flex items-start justify-between gap-4">
  71          <div>
  72            <h3 className="font-display text-[17px] font-700 tracking-[-0.02em] text-text">{title}</h3>
  73            {description && (
  74              <p className="mt-1 text-[13px] leading-[1.6] text-text-3/75">{description}</p>
  75            )}
  76          </div>
  77          {action}
  78        </div>
  79        {children}
  80      </section>
  81    )
  82  }
  83  
  84  function formatHbDuration(sec: number): string {
  85    if (sec >= 3600) {
  86      const h = Math.floor(sec / 3600)
  87      const m = Math.floor((sec % 3600) / 60)
  88      return m > 0 ? `${h}h${m}m` : `${h}h`
  89    }
  90    if (sec >= 60) return `${Math.floor(sec / 60)}m`
  91    return `${sec}s`
  92  }
  93  
  94  /** Parse a stored heartbeatInterval string or heartbeatIntervalSec number to a select-friendly string of seconds */
  95  function parseDurationToSec(interval: string | number | null | undefined, intervalSec: number | null | undefined): string {
  96    if (intervalSec != null && Number.isFinite(intervalSec) && intervalSec > 0) {
  97      // Snap to nearest preset if close, otherwise use raw value
  98      const closest = HB_PRESETS.find((p) => p === Math.round(intervalSec))
  99      if (closest) return String(closest)
 100    }
 101    if (typeof interval === 'number' && Number.isFinite(interval) && interval > 0) {
 102      return String(Math.round(interval))
 103    }
 104    if (interval != null && typeof interval === 'string' && interval.trim()) {
 105      const t = interval.trim().toLowerCase()
 106      const n = Number(t)
 107      if (Number.isFinite(n) && n > 0) return String(Math.round(n))
 108      const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/)
 109      if (m && (m[1] || m[2] || m[3])) {
 110        const total = (m[1] ? parseInt(m[1]) * 3600 : 0) + (m[2] ? parseInt(m[2]) * 60 : 0) + (m[3] ? parseInt(m[3]) : 0)
 111        if (total > 0) return String(total)
 112      }
 113    }
 114    return '' // default
 115  }
 116  
 117  function formatIdentityList(value: string[] | null | undefined): string {
 118    return Array.isArray(value) ? value.join('\n') : ''
 119  }
 120  
 121  function parseIdentityList(value: string): string[] {
 122    const seen = new Set<string>()
 123    return value
 124      .split('\n')
 125      .map((line) => line.replace(/\s+/g, ' ').trim())
 126      .filter((line) => {
 127        if (!line) return false
 128        const key = line.toLowerCase()
 129        if (seen.has(key)) return false
 130        seen.add(key)
 131        return true
 132      })
 133  }
 134  
 135  function formatGatewayTagList(value: string[] | null | undefined): string {
 136    return Array.isArray(value) ? value.join(', ') : ''
 137  }
 138  
 139  function parseGatewayTagList(value: string): string[] {
 140    const seen = new Set<string>()
 141    return value
 142      .split(/[,\n]/)
 143      .map((entry) => entry.trim())
 144      .filter((entry) => {
 145        if (!entry) return false
 146        const key = entry.toLowerCase()
 147        if (seen.has(key)) return false
 148        seen.add(key)
 149        return true
 150      })
 151  }
 152  
 153  export function AgentSheet() {
 154    const open = useAppStore((s) => s.agentSheetOpen)
 155    const setOpen = useAppStore((s) => s.setAgentSheetOpen)
 156    const editingId = useAppStore((s) => s.editingAgentId)
 157    const setEditingId = useAppStore((s) => s.setEditingAgentId)
 158    const agents = useAppStore((s) => s.agents)
 159    const loadAgents = useAppStore((s) => s.loadAgents)
 160    const updateAgentInStore = useAppStore((s) => s.updateAgentInStore)
 161    const activeSessionId = useAppStore(selectActiveSessionId)
 162    const currentSession = useAppStore((s) => {
 163      const id = selectActiveSessionId(s)
 164      return id ? s.sessions[id] : null
 165    })
 166    const refreshSession = useAppStore((s) => s.refreshSession)
 167    const projects = useAppStore((s) => s.projects)
 168    const loadProjects = useAppStore((s) => s.loadProjects)
 169    const providers = useAppStore((s) => s.providers)
 170    const loadProviders = useAppStore((s) => s.loadProviders)
 171    const providerConfigs = useAppStore((s) => s.providerConfigs)
 172    const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
 173    const gatewayProfiles = useAppStore((s) => s.gatewayProfiles)
 174    const loadGatewayProfiles = useAppStore((s) => s.loadGatewayProfiles)
 175    const credentials = useAppStore((s) => s.credentials)
 176    const loadCredentials = useAppStore((s) => s.loadCredentials)
 177    const appSettings = useAppStore((s) => s.appSettings)
 178    const loadSettings = useAppStore((s) => s.loadSettings)
 179    const dynamicSkills = useAppStore((s) => s.skills)
 180    const mcpServers = useAppStore((s) => s.mcpServers)
 181    const loadSkills = useAppStore((s) => s.loadSkills)
 182    const loadMcpServersAction = useAppStore((s) => s.loadMcpServers)
 183    const [claudeSkills, setClaudeSkills] = useState<ClaudeSkill[]>([])
 184    const [claudeSkillsLoading, setClaudeSkillsLoading] = useState(false)
 185    const loadClaudeSkills = async () => {
 186      setClaudeSkillsLoading(true)
 187      try {
 188        const skills = await api<ClaudeSkill[]>('GET', '/claude-skills')
 189        setClaudeSkills(skills)
 190      } catch { /* ignore */ }
 191      finally { setClaudeSkillsLoading(false) }
 192    }
 193  
 194    const [name, setName] = useState('')
 195    const [description, setDescription] = useState('')
 196    const [soul, setSoul] = useState('')
 197    const [soulInitial, setSoulInitial] = useState('')
 198    const [soulSaveState, setSoulSaveState] = useState<'idle' | 'saved'>('idle')
 199    const [systemPrompt, setSystemPrompt] = useState('')
 200    const [provider, setProvider] = useState<AgentProviderId>('claude-cli')
 201    const [model, setModel] = useState('')
 202    const [credentialId, setCredentialId] = useState<string | null>(null)
 203    const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
 204    const [gatewayProfileId, setGatewayProfileId] = useState<string | null>(null)
 205    const [preferredGatewayTagsText, setPreferredGatewayTagsText] = useState('')
 206    const [preferredGatewayUseCase, setPreferredGatewayUseCase] = useState('')
 207    const [routingStrategy, setRoutingStrategy] = useState<AgentRoutingStrategy>('single')
 208    const [routingTargets, setRoutingTargets] = useState<AgentRoutingTarget[]>([])
 209    const [role, setRole] = useState<'worker' | 'coordinator'>('worker')
 210    const [delegationEnabled, setDelegationEnabled] = useState(false)
 211    const [delegationTargetMode, setDelegationTargetMode] = useState<'all' | 'selected'>('all')
 212    const [delegationTargetAgentIds, setDelegationTargetAgentIds] = useState<string[]>([])
 213    const [tools, setTools] = useState<string[]>([])
 214    // Scoped tool access is the default for new agents (cuts ~3 k input tokens
 215    // per turn). Existing agents with no toolAccessMode field persisted stay
 216    // universal server-side for backward compat; the new-agent setup path
 217    // below also explicitly writes 'scoped' so it persists on save.
 218    const [toolAccessMode, setToolAccessMode] = useState<'universal' | 'scoped'>('scoped')
 219    const [extensions, setExtensions] = useState<string[]>([])
 220    const [enabledExtensionIds, setEnabledExtensionIds] = useState<Set<string> | null>(null)
 221    const [skills, setSkills] = useState<string[]>([])
 222    const [skillIds, setSkillIds] = useState<string[]>([])
 223    const [mcpServerIds, setMcpServerIds] = useState<string[]>([])
 224    const [mcpDisabledTools, setMcpDisabledTools] = useState<string[]>([])
 225    const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
 226    const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
 227    const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
 228    const [capabilities, setCapabilities] = useState<string[]>([])
 229    const [capInput, setCapInput] = useState('')
 230    const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
 231    const [openclawEnabled, setOpenclawEnabled] = useState(false)
 232    const [projectId, setProjectId] = useState<string | undefined>(undefined)
 233    const [avatarSeed, setAvatarSeed] = useState('')
 234    const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
 235    const [uploading, setUploading] = useState(false)
 236    const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
 237    const [memoryScopeMode, setMemoryScopeMode] = useState<'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'>('auto')
 238    const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
 239    const [proactiveMemory, setProactiveMemory] = useState(true)
 240    const [autoDraftSkillSuggestions, setAutoDraftSkillSuggestions] = useState(true)
 241    const [autoRecovery, setAutoRecovery] = useState(false)
 242    const [disabled, setDisabled] = useState(false)
 243    const [filesystemScope, setFilesystemScope] = useState<'workspace' | 'machine'>('workspace')
 244    const [voiceId, setVoiceId] = useState('')
 245    const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
 246    const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('')  // '' = default (30m)
 247    const [heartbeatModel, setHeartbeatModel] = useState('')
 248    const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
 249    const [dreamEnabled, setDreamEnabled] = useState(false)
 250    const [dreamCooldownMinutes, setDreamCooldownMinutes] = useState('360')
 251    const [dreamTier2Enabled, setDreamTier2Enabled] = useState(true)
 252    const [orchestratorEnabled, setOrchestratorEnabled] = useState(false)
 253    const [orchestratorMission, setOrchestratorMission] = useState('')
 254    const [orchestratorWakeInterval, setOrchestratorWakeInterval] = useState('5m')
 255    const [orchestratorGovernance, setOrchestratorGovernance] = useState<'autonomous' | 'approval-required' | 'notify-only'>('autonomous')
 256    const [orchestratorMaxCyclesPerDay, setOrchestratorMaxCyclesPerDay] = useState<string>('')
 257    const [sessionResetMode, setSessionResetMode] = useState<'' | 'idle' | 'daily' | 'isolated'>('')
 258    const [sessionIdleTimeoutSec, setSessionIdleTimeoutSec] = useState('')
 259    const [sessionMaxAgeSec, setSessionMaxAgeSec] = useState('')
 260    const [sessionDailyResetAt, setSessionDailyResetAt] = useState('')
 261    const [sessionResetTimezone, setSessionResetTimezone] = useState('')
 262    const [identityPersonaLabel, setIdentityPersonaLabel] = useState('')
 263    const [identitySelfSummary, setIdentitySelfSummary] = useState('')
 264    const [identityRelationshipSummary, setIdentityRelationshipSummary] = useState('')
 265    const [identityToneStyle, setIdentityToneStyle] = useState('')
 266    const [identityBoundariesText, setIdentityBoundariesText] = useState('')
 267    const [identityContinuityNotesText, setIdentityContinuityNotesText] = useState('')
 268    const [budgetEnabled, setBudgetEnabled] = useState(false)
 269    const [hourlyBudget, setHourlyBudget] = useState('')
 270    const [dailyBudget, setDailyBudget] = useState('')
 271    const [monthlyBudget, setMonthlyBudget] = useState('')
 272    const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
 273    const [addingKey, setAddingKey] = useState(false)
 274    const [newKeyName, setNewKeyName] = useState('')
 275    const [newKeyValue, setNewKeyValue] = useState('')
 276    const [savingKey, setSavingKey] = useState(false)
 277  
 278    // Test connection state
 279    const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
 280    const [testMessage, setTestMessage] = useState('')
 281    const [testErrorCode, setTestErrorCode] = useState<string | null>(null)
 282    const [testDeviceId, setTestDeviceId] = useState<string | null>(null)
 283    const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
 284    const [configCopied, setConfigCopied] = useState(false)
 285  
 286    const soulFileRef = useRef<HTMLInputElement>(null)
 287    const [soulLibraryOpen, setSoulLibraryOpen] = useState(false)
 288    const promptFileRef = useRef<HTMLInputElement>(null)
 289    const importFileRef = useRef<HTMLInputElement>(null)
 290    const lastAutoSyncedModelsKeyRef = useRef<string | null>(null)
 291    const skipAutoModelRef = useRef(false)
 292    const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
 293  
 294    const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
 295      const file = e.target.files?.[0]
 296      if (!file) return
 297      const reader = new FileReader()
 298      reader.onload = (ev) => setter(ev.target?.result as string)
 299      reader.readAsText(file)
 300      e.target.value = ''
 301    }
 302  
 303    const agentSelectableProviders = useMemo(
 304      () => buildAgentSelectableProviders(providers, providerConfigs),
 305      [providers, providerConfigs],
 306    )
 307    const currentProvider = agentSelectableProviders.find((p) => p.id === provider)
 308    const providerCredentials = useMemo(
 309      () => resolveAgentSelectableProviderCredentials(provider, credentials, providerConfigs),
 310      [credentials, provider, providerConfigs],
 311    )
 312    const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
 313    const openclawGatewayProfiles = gatewayProfiles.filter((item) => item.provider === 'openclaw')
 314    const setAgentPrefill = useAppStore((s) => s.setAgentPrefill)
 315    const editing = editingId ? agents[editingId] : null
 316    const hasNativeCapabilities = NATIVE_CAPABILITY_PROVIDER_IDS.has(provider)
 317    const globalVoiceId = typeof appSettings.elevenLabsVoiceId === 'string' ? appSettings.elevenLabsVoiceId.trim() : ''
 318    const agentVoiceId = voiceId.trim()
 319    const elevenLabsConfigured = appSettings.elevenLabsApiKeyConfigured === true
 320    const voiceControlsAvailable = elevenLabsConfigured || appSettings.elevenLabsEnabled === true || !!globalVoiceId || !!agentVoiceId
 321    const voicePlaybackEnabled = appSettings.elevenLabsEnabled === true
 322    const effectiveVoiceId = agentVoiceId || globalVoiceId || FALLBACK_ELEVENLABS_VOICE_ID
 323    const effectiveVoiceSource = agentVoiceId
 324      ? 'Agent override'
 325      : globalVoiceId
 326        ? 'Global default'
 327        : 'Built-in fallback'
 328    const syncLiveProviderModels = useCallback(async (
 329      providerId: string,
 330      nextCredentialId: string | null,
 331      nextEndpoint: string | null,
 332      nextOllamaMode: 'local' | 'cloud',
 333      force = false,
 334    ): Promise<{ synced: boolean; models: string[] } | null> => {
 335      if (openclawEnabled) return null
 336      if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(providerId as ProviderType)) return null
 337      const providerInfo = agentSelectableProviders.find((item) => item.id === providerId)
 338      if (!providerInfo?.supportsModelDiscovery) return null
 339  
 340      const result = await fetchProviderModelDiscovery({
 341        providerId,
 342        credentialId: nextCredentialId,
 343        endpoint: nextEndpoint,
 344        ollamaMode: providerId === 'ollama' ? nextOllamaMode : null,
 345        force,
 346      })
 347  
 348      if (!result.ok || result.models.length === 0) return { synced: false, models: result.models }
 349  
 350      const sameModels = providerInfo.models.length === result.models.length
 351        && providerInfo.models.every((item, index) => item === result.models[index])
 352  
 353      if (!sameModels) {
 354        await api('PUT', `/providers/${providerId}/models`, { models: result.models })
 355        await loadProviders()
 356      }
 357  
 358      setModel((currentModel) => currentModel.trim() || result.models[0] || '')
 359      return { synced: !sameModels, models: result.models }
 360    }, [agentSelectableProviders, loadProviders, openclawEnabled])
 361  
 362    const providerNeedsKey = !editing && (
 363      (currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
 364      (provider === 'ollama' && ollamaMode === 'cloud' && providerCredentials.length === 0 && !addingKey)
 365    )
 366  
 367    useEffect(() => {
 368      if (!open) {
 369        lastAutoSyncedModelsKeyRef.current = null
 370        return
 371      }
 372      if (openclawEnabled) return
 373      if (!AUTO_SYNC_MODEL_PROVIDER_IDS.has(provider as ProviderType)) return
 374      if (!currentProvider?.supportsModelDiscovery) return
 375  
 376      const requiresCredential = currentProvider.requiresApiKey || (provider === 'ollama' && ollamaMode === 'cloud')
 377      if (requiresCredential && !credentialId) return
 378  
 379      const syncKey = `${provider}::${credentialId || ''}::${apiEndpoint?.trim() || ''}::${provider === 'ollama' ? ollamaMode : ''}`
 380      if (lastAutoSyncedModelsKeyRef.current === syncKey) return
 381      lastAutoSyncedModelsKeyRef.current = syncKey
 382  
 383      void syncLiveProviderModels(provider, credentialId, apiEndpoint, ollamaMode, false).catch(() => {})
 384    }, [apiEndpoint, credentialId, currentProvider, ollamaMode, open, openclawEnabled, provider, syncLiveProviderModels])
 385  
 386    useEffect(() => {
 387      if (open) {
 388        loadSettings()
 389        loadProviders()
 390        loadProviderConfigs()
 391        loadGatewayProfiles()
 392        loadCredentials()
 393        loadSkills()
 394        loadMcpServersAction()
 395        loadProjects()
 396        loadClaudeSkills()
 397        // Fetch enabled extension IDs so we can filter tool toggles
 398        api<{ enabledExtensionIds: string[] }>('GET', '/extensions/builtins')
 399          .then((res) => { if (res?.enabledExtensionIds) setEnabledExtensionIds(new Set(res.enabledExtensionIds)) })
 400          .catch(() => {})
 401        setTestStatus('idle')
 402        setTestMessage('')
 403        setShowAdvancedSettings(false)
 404        if (editing) {
 405          setName(editing.name)
 406          setDescription(editing.description)
 407          setSoul(editing.soul || '')
 408          setSoulInitial(editing.soul || '')
 409          setSoulSaveState('idle')
 410          setSystemPrompt(editing.systemPrompt)
 411          setProvider(editing.provider)
 412          setModel(editing.model)
 413          setCredentialId(editing.credentialId || null)
 414          setApiEndpoint(editing.apiEndpoint || null)
 415          setGatewayProfileId(editing.gatewayProfileId || null)
 416          setPreferredGatewayTagsText(formatGatewayTagList(editing.preferredGatewayTags))
 417          setPreferredGatewayUseCase(editing.preferredGatewayUseCase || '')
 418          setRoutingStrategy(editing.routingStrategy || 'single')
 419          setRoutingTargets(editing.routingTargets || [])
 420          setRole(editing.role === 'coordinator' ? 'coordinator' : 'worker')
 421          setDelegationEnabled(editing.delegationEnabled === true)
 422          setDelegationTargetMode(editing.delegationTargetMode === 'selected' ? 'selected' : 'all')
 423          setDelegationTargetAgentIds(editing.delegationTargetAgentIds || [])
 424          setTools(getEnabledToolIds(editing))
 425          setToolAccessMode(editing.toolAccessMode === 'scoped' ? 'scoped' : 'universal')
 426          setExtensions(getEnabledExtensionIds(editing))
 427          setSkills(editing.skills || [])
 428          setSkillIds(editing.skillIds || [])
 429          setMcpServerIds(editing.mcpServerIds || [])
 430          setMcpDisabledTools(editing.mcpDisabledTools || [])
 431          setFallbackCredentialIds(editing.fallbackCredentialIds || [])
 432          setCapabilities(Array.isArray(editing.capabilities) ? editing.capabilities : [])
 433          setCapInput('')
 434          setOllamaMode(resolveStoredOllamaMode({
 435            ollamaMode: editing.ollamaMode ?? null,
 436            apiEndpoint: editing.apiEndpoint ?? null,
 437          }))
 438          setOpenclawEnabled(editing.provider === 'openclaw')
 439          setProjectId(editing.projectId)
 440          setAvatarSeed(editing.avatarSeed || Math.random().toString(36).slice(2, 10))
 441          setAvatarUrl(editing.avatarUrl || null)
 442          setThinkingLevel(editing.thinkingLevel || '')
 443          setMemoryScopeMode(editing.memoryScopeMode || 'auto')
 444          setMemoryTierPreference(editing.memoryTierPreference || 'blended')
 445          setProactiveMemory(editing.proactiveMemory !== false)
 446          setAutoDraftSkillSuggestions(editing.autoDraftSkillSuggestions !== false)
 447          setAutoRecovery(editing.autoRecovery || false)
 448          setDisabled(editing.disabled === true)
 449          setFilesystemScope(editing.filesystemScope === 'machine' ? 'machine' : 'workspace')
 450          setVoiceId(editing.elevenLabsVoiceId || '')
 451          setHeartbeatEnabled(editing.heartbeatEnabled || false)
 452          setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
 453          setHeartbeatModel(editing.heartbeatModel || '')
 454          setHeartbeatPrompt(editing.heartbeatPrompt || '')
 455          setDreamEnabled(editing.dreamEnabled || false)
 456          setDreamCooldownMinutes(editing.dreamConfig?.cooldownMinutes != null ? String(editing.dreamConfig.cooldownMinutes) : '360')
 457          setDreamTier2Enabled(editing.dreamConfig?.tier2Enabled !== false)
 458          setOrchestratorEnabled(editing.orchestratorEnabled || false)
 459          setOrchestratorMission(editing.orchestratorMission || '')
 460          setOrchestratorWakeInterval(typeof editing.orchestratorWakeInterval === 'string' ? editing.orchestratorWakeInterval : typeof editing.orchestratorWakeInterval === 'number' ? `${editing.orchestratorWakeInterval}s` : '5m')
 461          setOrchestratorGovernance(editing.orchestratorGovernance || 'autonomous')
 462          setOrchestratorMaxCyclesPerDay(editing.orchestratorMaxCyclesPerDay != null ? String(editing.orchestratorMaxCyclesPerDay) : '')
 463          setSessionResetMode(editing.sessionResetMode || '')
 464          setSessionIdleTimeoutSec(editing.sessionIdleTimeoutSec != null ? String(editing.sessionIdleTimeoutSec) : '')
 465          setSessionMaxAgeSec(editing.sessionMaxAgeSec != null ? String(editing.sessionMaxAgeSec) : '')
 466          setSessionDailyResetAt(editing.sessionDailyResetAt || '')
 467          setSessionResetTimezone(editing.sessionResetTimezone || '')
 468          setIdentityPersonaLabel(editing.identityState?.personaLabel || '')
 469          setIdentitySelfSummary(editing.identityState?.selfSummary || '')
 470          setIdentityRelationshipSummary(editing.identityState?.relationshipSummary || '')
 471          setIdentityToneStyle(editing.identityState?.toneStyle || '')
 472          setIdentityBoundariesText(formatIdentityList(editing.identityState?.boundaries))
 473          setIdentityContinuityNotesText(formatIdentityList(editing.identityState?.continuityNotes))
 474          setBudgetEnabled(
 475            (typeof editing.hourlyBudget === 'number' && editing.hourlyBudget > 0)
 476            || (typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0)
 477            || (typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0),
 478          )
 479          setHourlyBudget(typeof editing.hourlyBudget === 'number' && editing.hourlyBudget > 0 ? String(editing.hourlyBudget) : '')
 480          setDailyBudget(typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0 ? String(editing.dailyBudget) : '')
 481          setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
 482          setBudgetAction(editing.budgetAction || 'warn')
 483        } else if (useAppStore.getState().agentPrefill) {
 484          // Duplicate mode — prefill from source agent, then clear
 485          const src = useAppStore.getState().agentPrefill!
 486          setAgentPrefill(null)
 487          skipAutoModelRef.current = true
 488          setName(`${src.name || 'Agent'} (Copy)`)
 489          setDescription(src.description || '')
 490          setSoul(src.soul || '')
 491          setSoulInitial(src.soul || '')
 492          setSoulSaveState('idle')
 493          setSystemPrompt(src.systemPrompt || '')
 494          setProvider(src.provider || 'claude-cli')
 495          setModel(src.model || '')
 496          setCredentialId(src.credentialId || null)
 497          setApiEndpoint(src.apiEndpoint || null)
 498          setGatewayProfileId(src.gatewayProfileId || null)
 499          setPreferredGatewayTagsText(formatGatewayTagList(src.preferredGatewayTags))
 500          setPreferredGatewayUseCase(src.preferredGatewayUseCase || '')
 501          setRoutingStrategy(src.routingStrategy || 'single')
 502          setRoutingTargets(src.routingTargets || [])
 503          setRole(src.role === 'coordinator' ? 'coordinator' : 'worker')
 504          setDelegationEnabled(src.delegationEnabled === true)
 505          setDelegationTargetMode(src.delegationTargetMode === 'selected' ? 'selected' : 'all')
 506          setDelegationTargetAgentIds(src.delegationTargetAgentIds || [])
 507          setTools(getEnabledToolIds(src))
 508          setToolAccessMode(src.toolAccessMode === 'scoped' ? 'scoped' : 'universal')
 509          setExtensions(getEnabledExtensionIds(src))
 510          setSkills(src.skills || [])
 511          setSkillIds(src.skillIds || [])
 512          setMcpServerIds(src.mcpServerIds || [])
 513          setMcpDisabledTools(src.mcpDisabledTools || [])
 514          setFallbackCredentialIds(src.fallbackCredentialIds || [])
 515          setCapabilities(Array.isArray(src.capabilities) ? src.capabilities : [])
 516          setCapInput('')
 517          setOllamaMode(resolveStoredOllamaMode({
 518            ollamaMode: src.ollamaMode ?? null,
 519            apiEndpoint: src.apiEndpoint ?? null,
 520          }))
 521          setOpenclawEnabled(src.provider === 'openclaw')
 522          setProjectId(src.projectId)
 523          setAvatarSeed(Math.random().toString(36).slice(2, 10))
 524          setAvatarUrl(null)
 525          setThinkingLevel(src.thinkingLevel || '')
 526          setMemoryScopeMode(src.memoryScopeMode || 'auto')
 527          setMemoryTierPreference(src.memoryTierPreference || 'blended')
 528          setProactiveMemory(src.proactiveMemory !== false)
 529          setAutoDraftSkillSuggestions(src.autoDraftSkillSuggestions !== false)
 530          setAutoRecovery(src.autoRecovery || false)
 531          setDisabled(false)
 532          setFilesystemScope(src.filesystemScope === 'machine' ? 'machine' : 'workspace')
 533          setVoiceId(src.elevenLabsVoiceId || '')
 534          setHeartbeatEnabled(src.heartbeatEnabled || false)
 535          setHeartbeatIntervalSec(parseDurationToSec(src.heartbeatInterval, src.heartbeatIntervalSec))
 536          setHeartbeatModel(src.heartbeatModel || '')
 537          setHeartbeatPrompt(src.heartbeatPrompt || '')
 538          setDreamEnabled(src.dreamEnabled || false)
 539          setDreamCooldownMinutes(src.dreamConfig?.cooldownMinutes != null ? String(src.dreamConfig.cooldownMinutes) : '360')
 540          setDreamTier2Enabled(src.dreamConfig?.tier2Enabled !== false)
 541          setOrchestratorEnabled(src.orchestratorEnabled || false)
 542          setOrchestratorMission(src.orchestratorMission || '')
 543          setOrchestratorWakeInterval(typeof src.orchestratorWakeInterval === 'string' ? src.orchestratorWakeInterval : typeof src.orchestratorWakeInterval === 'number' ? `${src.orchestratorWakeInterval}s` : '5m')
 544          setOrchestratorGovernance(src.orchestratorGovernance || 'autonomous')
 545          setOrchestratorMaxCyclesPerDay(src.orchestratorMaxCyclesPerDay != null ? String(src.orchestratorMaxCyclesPerDay) : '')
 546          setSessionResetMode(src.sessionResetMode || '')
 547          setSessionIdleTimeoutSec(src.sessionIdleTimeoutSec != null ? String(src.sessionIdleTimeoutSec) : '')
 548          setSessionMaxAgeSec(src.sessionMaxAgeSec != null ? String(src.sessionMaxAgeSec) : '')
 549          setSessionDailyResetAt(src.sessionDailyResetAt || '')
 550          setSessionResetTimezone(src.sessionResetTimezone || '')
 551          setIdentityPersonaLabel(src.identityState?.personaLabel || '')
 552          setIdentitySelfSummary(src.identityState?.selfSummary || '')
 553          setIdentityRelationshipSummary(src.identityState?.relationshipSummary || '')
 554          setIdentityToneStyle(src.identityState?.toneStyle || '')
 555          setIdentityBoundariesText(formatIdentityList(src.identityState?.boundaries))
 556          setIdentityContinuityNotesText(formatIdentityList(src.identityState?.continuityNotes))
 557          setBudgetEnabled(
 558            (typeof src.hourlyBudget === 'number' && src.hourlyBudget > 0)
 559            || (typeof src.dailyBudget === 'number' && src.dailyBudget > 0)
 560            || (typeof src.monthlyBudget === 'number' && src.monthlyBudget > 0),
 561          )
 562          setHourlyBudget(typeof src.hourlyBudget === 'number' && src.hourlyBudget > 0 ? String(src.hourlyBudget) : '')
 563          setDailyBudget(typeof src.dailyBudget === 'number' && src.dailyBudget > 0 ? String(src.dailyBudget) : '')
 564          setMonthlyBudget(typeof src.monthlyBudget === 'number' && src.monthlyBudget > 0 ? String(src.monthlyBudget) : '')
 565          setBudgetAction(src.budgetAction || 'warn')
 566        } else {
 567          setName('')
 568          setDescription('')
 569          const newSoul = randomSoul()
 570          setSoul(newSoul)
 571          setSoulInitial(newSoul)
 572          setSoulSaveState('idle')
 573          setSystemPrompt('')
 574          setProvider('claude-cli')
 575          setModel('')
 576          setCredentialId(null)
 577          setApiEndpoint(null)
 578          setGatewayProfileId(null)
 579          setPreferredGatewayTagsText('')
 580          setPreferredGatewayUseCase('')
 581          setRoutingStrategy('single')
 582          setRoutingTargets([])
 583          setRole('worker')
 584          setDelegationEnabled(false)
 585          setDelegationTargetMode('all')
 586          setDelegationTargetAgentIds([])
 587          setTools(getDefaultAgentToolIds())
 588          setToolAccessMode('scoped')
 589          setExtensions([])
 590          setSkills([])
 591          setSkillIds([])
 592          setMcpDisabledTools([])
 593          setFallbackCredentialIds([])
 594          setCapabilities([])
 595          setCapInput('')
 596          setOllamaMode('local')
 597          setOpenclawEnabled(false)
 598          setProjectId(undefined)
 599          setAvatarSeed('')
 600          setThinkingLevel('')
 601          setMemoryScopeMode('auto')
 602          setMemoryTierPreference('blended')
 603          setProactiveMemory(true)
 604          setAutoDraftSkillSuggestions(true)
 605          setAutoRecovery(false)
 606          setDisabled(false)
 607          setVoiceId('')
 608          setHeartbeatEnabled(true)
 609          setHeartbeatIntervalSec('')
 610          setHeartbeatModel('')
 611          setHeartbeatPrompt('')
 612          setOrchestratorEnabled(false)
 613          setOrchestratorMission('')
 614          setOrchestratorWakeInterval('5m')
 615          setOrchestratorGovernance('autonomous')
 616          setOrchestratorMaxCyclesPerDay('')
 617          setSessionResetMode('')
 618          setSessionIdleTimeoutSec('')
 619          setSessionMaxAgeSec('')
 620          setSessionDailyResetAt('')
 621          setSessionResetTimezone('')
 622          setIdentityPersonaLabel('')
 623          setIdentitySelfSummary('')
 624          setIdentityRelationshipSummary('')
 625          setIdentityToneStyle('')
 626          setIdentityBoundariesText('')
 627          setIdentityContinuityNotesText('')
 628          setBudgetEnabled(false)
 629          setHourlyBudget('')
 630          setDailyBudget('')
 631          setMonthlyBudget('')
 632          setBudgetAction('warn')
 633        }
 634      }
 635    // eslint-disable-next-line react-hooks/exhaustive-deps
 636    }, [open, editingId])
 637  
 638    useEffect(() => {
 639      if (skipAutoModelRef.current) {
 640        skipAutoModelRef.current = false
 641        return
 642      }
 643      if (currentProvider?.models.length && !editing) {
 644        setModel(currentProvider.models[0])
 645      }
 646    // eslint-disable-next-line react-hooks/exhaustive-deps
 647    }, [provider, agentSelectableProviders])
 648  
 649    // Reset test status when connection params change
 650    useEffect(() => {
 651      setTestStatus('idle')
 652      setTestMessage('')
 653    }, [provider, credentialId, apiEndpoint])
 654  
 655    // Fetch MCP tools when selected servers change
 656    useEffect(() => {
 657      if (!mcpServerIds.length) {
 658        setMcpTools({})
 659        return
 660      }
 661      let cancelled = false
 662      setMcpToolsLoading(true)
 663      Promise.all(
 664        mcpServerIds.map(async (id) => {
 665          try {
 666            const tools = await api<{ name: string; description: string }[]>('GET', `/mcp-servers/${id}/tools`)
 667            return { id, tools: Array.isArray(tools) ? tools : [] }
 668          } catch {
 669            return { id, tools: [] }
 670          }
 671        })
 672      ).then((results) => {
 673        if (cancelled) return
 674        const map: Record<string, { name: string; description: string }[]> = {}
 675        for (const r of results) map[r.id] = r.tools
 676        setMcpTools(map)
 677        setMcpToolsLoading(false)
 678      })
 679      return () => { cancelled = true }
 680    // eslint-disable-next-line react-hooks/exhaustive-deps
 681    }, [mcpServerIds.join(',')])
 682  
 683    // Fetch OpenClaw device ID when toggle is enabled
 684    useEffect(() => {
 685      if (!openclawEnabled) return
 686      let cancelled = false
 687      api<{ deviceId: string }>('GET', '/setup/openclaw-device').then((res) => {
 688        if (!cancelled && res.deviceId) setOpenclawDeviceId(res.deviceId)
 689      }).catch(() => {})
 690      return () => { cancelled = true }
 691    }, [openclawEnabled])
 692  
 693    const onClose = () => {
 694      setOpen(false)
 695      setEditingId(null)
 696    }
 697  
 698    const applyGatewayProfileSelection = (nextGatewayProfileId: string | null) => {
 699      setGatewayProfileId(nextGatewayProfileId)
 700      const gateway = openclawGatewayProfiles.find((item) => item.id === nextGatewayProfileId)
 701      if (!gateway) return
 702      setProvider('openclaw')
 703      setOpenclawEnabled(true)
 704      setApiEndpoint(gateway.endpoint)
 705      if (gateway.credentialId) setCredentialId(gateway.credentialId)
 706      if (!model) setModel('default')
 707    }
 708  
 709    const updateRoutingTarget = (targetId: string, patch: Partial<AgentRoutingTarget>) => {
 710      setRoutingTargets((current) => current.map((target) => (
 711        target.id === targetId
 712          ? { ...target, ...patch }
 713          : target
 714      )))
 715    }
 716  
 717    const removeRoutingTarget = (targetId: string) => {
 718      setRoutingTargets((current) => current.filter((target) => target.id !== targetId))
 719    }
 720  
 721    const addRoutingTargetFromCurrent = () => {
 722      const nextTarget: AgentRoutingTarget = {
 723        id: Math.random().toString(16).slice(2, 10),
 724        label: routingTargets.length === 0 ? 'Primary route' : `Route ${routingTargets.length + 1}`,
 725        role: routingTargets.length === 0 ? 'primary' : 'backup',
 726        provider,
 727        model,
 728        ollamaMode: provider === 'ollama' ? ollamaMode : null,
 729        credentialId,
 730        fallbackCredentialIds,
 731        apiEndpoint,
 732        gatewayProfileId,
 733        preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
 734        preferredGatewayUseCase: preferredGatewayUseCase || null,
 735        priority: routingTargets.length + 1,
 736      }
 737      setRoutingTargets((current) => [...current, nextTarget])
 738    }
 739  
 740    const handleSave = async () => {
 741      // For any endpoint, just ensure bare host:port gets a protocol prepended
 742      let normalizedEndpoint = apiEndpoint
 743      if (normalizedEndpoint) {
 744        const url = normalizedEndpoint.trim().replace(/\/+$/, '')
 745        normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
 746      }
 747      const parsedHourlyBudget = budgetEnabled && hourlyBudget ? Number(hourlyBudget) : null
 748      const parsedDailyBudget = budgetEnabled && dailyBudget ? Number(dailyBudget) : null
 749      const parsedMonthlyBudget = budgetEnabled && monthlyBudget ? Number(monthlyBudget) : null
 750      const parsedSessionIdleTimeoutSec = sessionIdleTimeoutSec ? Number(sessionIdleTimeoutSec) : null
 751      const parsedSessionMaxAgeSec = sessionMaxAgeSec ? Number(sessionMaxAgeSec) : null
 752      const identityBoundaries = parseIdentityList(identityBoundariesText)
 753      const identityContinuityNotes = parseIdentityList(identityContinuityNotesText)
 754      const identityState = (() => {
 755        const value = {
 756          personaLabel: identityPersonaLabel.trim() || undefined,
 757          selfSummary: identitySelfSummary.trim() || undefined,
 758          relationshipSummary: identityRelationshipSummary.trim() || undefined,
 759          toneStyle: identityToneStyle.trim() || undefined,
 760          boundaries: identityBoundaries.length ? identityBoundaries : undefined,
 761          continuityNotes: identityContinuityNotes.length ? identityContinuityNotes : undefined,
 762        }
 763        return Object.values(value).some((entry) => Array.isArray(entry) ? entry.length > 0 : Boolean(entry)) ? value : null
 764      })()
 765      const data = {
 766        name: name.trim() || 'Unnamed Agent',
 767        description,
 768        soul,
 769        systemPrompt,
 770        provider,
 771        model,
 772        ollamaMode: provider === 'ollama' ? ollamaMode : null,
 773        credentialId,
 774        apiEndpoint: normalizedEndpoint,
 775        gatewayProfileId,
 776        preferredGatewayTags: parseGatewayTagList(preferredGatewayTagsText),
 777        preferredGatewayUseCase: preferredGatewayUseCase || null,
 778        routingStrategy,
 779        routingTargets: routingTargets.map((target, index) => ({
 780          ...target,
 781          ollamaMode: target.provider === 'ollama'
 782            ? resolveStoredOllamaMode({
 783              ollamaMode: target.ollamaMode ?? null,
 784              apiEndpoint: target.apiEndpoint ?? null,
 785            })
 786            : null,
 787          preferredGatewayTags: parseGatewayTagList(formatGatewayTagList(target.preferredGatewayTags)),
 788          preferredGatewayUseCase: target.preferredGatewayUseCase || null,
 789          priority: typeof target.priority === 'number' ? target.priority : index + 1,
 790        })),
 791        role,
 792        delegationEnabled: role === 'coordinator' ? true : delegationEnabled,
 793        delegationTargetMode: delegationEnabled || role === 'coordinator' ? delegationTargetMode : 'all',
 794        delegationTargetAgentIds: (delegationEnabled || role === 'coordinator') && delegationTargetMode === 'selected' ? delegationTargetAgentIds : [],
 795        tools,
 796        toolAccessMode,
 797        extensions,
 798        skills,
 799        skillIds,
 800        mcpServerIds,
 801        mcpDisabledTools: mcpDisabledTools.length ? mcpDisabledTools : undefined,
 802        fallbackCredentialIds,
 803        capabilities,
 804        projectId: projectId || undefined,
 805        avatarSeed: avatarSeed.trim() || undefined,
 806        avatarUrl: avatarUrl || null,
 807        thinkingLevel: thinkingLevel || undefined,
 808        memoryScopeMode,
 809        memoryTierPreference,
 810        proactiveMemory,
 811        autoDraftSkillSuggestions,
 812        autoRecovery,
 813        disabled,
 814        filesystemScope: filesystemScope === 'machine' ? 'machine' as const : undefined,
 815        elevenLabsVoiceId: voiceId.trim() || null,
 816        heartbeatEnabled,
 817        heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
 818        heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
 819        heartbeatModel: heartbeatModel.trim() || null,
 820        heartbeatPrompt: heartbeatPrompt.trim() || null,
 821        dreamEnabled,
 822        dreamConfig: dreamEnabled
 823          ? { cooldownMinutes: Number(dreamCooldownMinutes) || 360, tier2Enabled: dreamTier2Enabled }
 824          : null,
 825        orchestratorEnabled,
 826        orchestratorMission: orchestratorMission.trim() || undefined,
 827        orchestratorWakeInterval: orchestratorWakeInterval.trim() || null,
 828        orchestratorGovernance,
 829        orchestratorMaxCyclesPerDay: orchestratorMaxCyclesPerDay ? Number(orchestratorMaxCyclesPerDay) : null,
 830        identityState,
 831        sessionResetMode: sessionResetMode || null,
 832        sessionIdleTimeoutSec: Number.isFinite(parsedSessionIdleTimeoutSec) && parsedSessionIdleTimeoutSec! >= 0 ? parsedSessionIdleTimeoutSec : null,
 833        sessionMaxAgeSec: Number.isFinite(parsedSessionMaxAgeSec) && parsedSessionMaxAgeSec! >= 0 ? parsedSessionMaxAgeSec : null,
 834        sessionDailyResetAt: sessionDailyResetAt.trim() || null,
 835        sessionResetTimezone: sessionResetTimezone.trim() || null,
 836        hourlyBudget: parsedHourlyBudget && parsedHourlyBudget > 0 ? parsedHourlyBudget : null,
 837        dailyBudget: parsedDailyBudget && parsedDailyBudget > 0 ? parsedDailyBudget : null,
 838        monthlyBudget: parsedMonthlyBudget && parsedMonthlyBudget > 0 ? parsedMonthlyBudget : null,
 839        budgetAction: budgetEnabled ? budgetAction : undefined,
 840      }
 841      if (WORKER_ONLY_PROVIDER_IDS.has(provider)) {
 842        data.role = 'worker'
 843        data.delegationEnabled = false
 844        data.heartbeatEnabled = false
 845        data.heartbeatInterval = null
 846        data.heartbeatIntervalSec = null
 847        data.heartbeatModel = null
 848        data.heartbeatPrompt = null
 849        data.dreamEnabled = false
 850        data.dreamConfig = null
 851      }
 852      const savedAgent = editing
 853        ? await updateAgent(editing.id, data)
 854        : await createAgent(data)
 855      updateAgentInStore(savedAgent)
 856      if (editing) {
 857        toast.success('Agent saved')
 858      } else {
 859        toast.success('Agent created')
 860      }
 861      await loadAgents()
 862      if (
 863        editing
 864        && activeSessionId
 865        && currentSession?.agentId === editing.id
 866        && (
 867          currentSession.shortcutForAgentId === editing.id
 868          || activeSessionId === editing.threadSessionId
 869        )
 870      ) {
 871        await refreshSession(activeSessionId)
 872      }
 873      setSoulInitial(soul)
 874      setSoulSaveState('saved')
 875      setTimeout(() => setSoulSaveState('idle'), 1500)
 876      onClose()
 877    }
 878  
 879    const handleDelete = async () => {
 880      if (editing) {
 881        await deleteAgent(editing.id)
 882        toast.success('Agent moved to trash')
 883        await loadAgents()
 884        onClose()
 885      }
 886    }
 887  
 888    const handleExport = () => {
 889      if (!editing) return
 890      const recommendedProviders = agentSelectableProviders.some((providerOption) => (
 891        providerOption.id === editing.provider && providerOption.type === 'builtin'
 892      ))
 893        ? [editing.provider as ProviderType]
 894        : undefined
 895      const pack: AgentPackManifest = {
 896        schemaVersion: 1,
 897        kind: 'swarmclaw-agent-pack',
 898        name: `${editing.name} Pack`,
 899        description: editing.description || undefined,
 900        exportedAt: Date.now(),
 901        recommendedProviders,
 902        agents: [{
 903          id: editing.name.replace(/\s+/g, '-').toLowerCase(),
 904          name: editing.name,
 905          description: editing.description || undefined,
 906          provider: editing.provider,
 907          model: editing.model,
 908          ollamaMode: editing.provider === 'ollama' ? (editing.ollamaMode || 'local') : null,
 909          credentialId: editing.credentialId || null,
 910          fallbackCredentialIds: editing.fallbackCredentialIds || [],
 911          apiEndpoint: editing.apiEndpoint || null,
 912          gatewayProfileId: editing.gatewayProfileId || null,
 913          routingStrategy: editing.routingStrategy || null,
 914          routingTargets: editing.routingTargets || [],
 915          tools: getEnabledToolIds(editing),
 916          extensions: getEnabledExtensionIds(editing),
 917          capabilities: editing.capabilities,
 918          elevenLabsVoiceId: editing.elevenLabsVoiceId || null,
 919          soul: editing.soul,
 920          systemPrompt: editing.systemPrompt,
 921        }],
 922      }
 923      const blob = new Blob([JSON.stringify(pack, null, 2)], { type: 'application/json' })
 924      const url = URL.createObjectURL(blob)
 925      const a = document.createElement('a')
 926      a.href = url
 927      a.download = `${editing.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.agent-pack.json`
 928      a.click()
 929      URL.revokeObjectURL(url)
 930      toast.success('Agent pack exported')
 931    }
 932  
 933    const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
 934      const file = e.target.files?.[0]
 935      if (!file) return
 936      const reader = new FileReader()
 937      reader.onload = async (ev) => {
 938        try {
 939          const data = JSON.parse(ev.target?.result as string)
 940          const importedAgent = data?.kind === 'swarmclaw-agent-pack'
 941            ? data?.agents?.[0]
 942            : data
 943          if (!importedAgent || typeof importedAgent !== 'object') throw new Error('Invalid agent pack')
 944          // Strip IDs and timestamps
 945          const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = importedAgent
 946          void [_id, _ca, _ua, _ts]
 947          await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
 948          await loadAgents()
 949          toast.success(data?.kind === 'swarmclaw-agent-pack' ? 'Agent pack imported' : 'Agent imported')
 950          onClose()
 951        } catch {
 952          toast.error('Invalid agent JSON file')
 953        }
 954      }
 955      reader.readAsText(file)
 956      e.target.value = ''
 957    }
 958  
 959    const handleTestConnection = async (): Promise<boolean> => {
 960      setTestStatus('testing')
 961      setTestMessage('')
 962      setTestErrorCode(null)
 963      try {
 964        const result = await api<{ ok: boolean; message: string; errorCode?: string; deviceId?: string }>('POST', '/setup/check-provider', {
 965          provider,
 966          credentialId,
 967          endpoint: apiEndpoint,
 968          model,
 969          ollamaMode: provider === 'ollama' ? ollamaMode : null,
 970        }, {
 971          timeoutMs: CONNECTION_TEST_TIMEOUT_MS,
 972        })
 973        if (result.deviceId) setTestDeviceId(result.deviceId)
 974        if (result.ok) {
 975          let syncedModels: string[] = []
 976          try {
 977            const synced = await syncLiveProviderModels(provider, credentialId, apiEndpoint, ollamaMode, true)
 978            syncedModels = synced?.models || []
 979          } catch {
 980            // Best-effort: a passing connection test should still pass if model sync fails.
 981          }
 982          setTestStatus('pass')
 983          setTestMessage(
 984            syncedModels.length > 0
 985              ? `${result.message} Synced ${syncedModels.length} live model${syncedModels.length === 1 ? '' : 's'} into the model picker.`
 986              : result.message,
 987          )
 988          return true
 989        } else {
 990          setTestStatus('fail')
 991          setTestMessage(result.message)
 992          setTestErrorCode(result.errorCode || null)
 993          toast.error(result.message || 'Connection test failed')
 994          return false
 995        }
 996      } catch (err: unknown) {
 997        const msg = err instanceof Error ? err.message : 'Connection test failed'
 998        setTestStatus('fail')
 999        setTestMessage(msg)
1000        toast.error(msg)
1001        return false
1002      }
1003    }
1004  
1005    // Whether this provider needs a connection test before saving.
1006    // Only CLI providers (no remote connection) skip the test.
1007    const needsTest = !providerNeedsKey && !NON_LANGGRAPH_PROVIDER_IDS.has(provider)
1008  
1009    const [saving, setSaving] = useState(false)
1010  
1011    const handleTestAndSave = async () => {
1012      if (needsTest) {
1013        const passed = await handleTestConnection()
1014        if (!passed) return
1015        if (!openclawEnabled) {
1016          // Brief pause so the user can see the success state on the button
1017          await sleep(1500)
1018        }
1019      }
1020      setSaving(true)
1021      await handleSave()
1022      setSaving(false)
1023    }
1024  
1025    const canDelegateToAgents = delegationEnabled || role === 'coordinator'
1026    const agentOptions = Object.values(agents).filter((p) => p.id !== editingId)
1027    const defaultAgentToolIds = useMemo(() => getDefaultAgentToolIds(), [])
1028    const toolsDifferFromDefault = tools.length !== defaultAgentToolIds.length
1029      || defaultAgentToolIds.some((toolId) => !tools.includes(toolId))
1030    const agentAdvancedBadges = useMemo(() => {
1031      const badges: string[] = []
1032      if (voiceId.trim()) badges.push('Voice')
1033      if (routingStrategy !== 'single' || routingTargets.length > 0 || fallbackCredentialIds.length > 0) badges.push('Routing')
1034      if (memoryScopeMode !== 'auto' || memoryTierPreference !== 'blended' || !proactiveMemory) badges.push('Memory')
1035      if (sessionResetMode || sessionIdleTimeoutSec || sessionMaxAgeSec || sessionDailyResetAt || sessionResetTimezone) badges.push('Session reset')
1036      if (identityPersonaLabel.trim() || identitySelfSummary.trim() || identityRelationshipSummary.trim() || identityToneStyle.trim()) badges.push('Continuity')
1037      if (skills.length > 0 || skillIds.length > 0 || mcpServerIds.length > 0 || mcpDisabledTools.length > 0) badges.push('Skills & MCP')
1038      if (toolsDifferFromDefault || filesystemScope === 'machine') badges.push('Tools')
1039      if (budgetEnabled) badges.push('Budget')
1040      if (dreamEnabled) badges.push('Dreaming')
1041      if (disabled) badges.push('Disabled')
1042      if (autoRecovery) badges.push('Recovery')
1043      if (projectId) badges.push('Project')
1044      if (thinkingLevel) badges.push('Thinking')
1045      if (!autoDraftSkillSuggestions) badges.push('Skill drafting')
1046      return Array.from(new Set(badges))
1047    }, [
1048      autoDraftSkillSuggestions,
1049      autoRecovery,
1050      budgetEnabled,
1051      disabled,
1052      dreamEnabled,
1053      fallbackCredentialIds.length,
1054      filesystemScope,
1055      identityPersonaLabel,
1056      identityRelationshipSummary,
1057      identitySelfSummary,
1058      identityToneStyle,
1059      mcpDisabledTools.length,
1060      mcpServerIds.length,
1061      memoryScopeMode,
1062      memoryTierPreference,
1063      proactiveMemory,
1064      projectId,
1065      routingStrategy,
1066      routingTargets.length,
1067      sessionDailyResetAt,
1068      sessionIdleTimeoutSec,
1069      sessionMaxAgeSec,
1070      sessionResetMode,
1071      sessionResetTimezone,
1072      skillIds.length,
1073      skills.length,
1074      thinkingLevel,
1075      toolsDifferFromDefault,
1076      voiceId,
1077    ])
1078    const advancedSummary = agentAdvancedBadges.length > 0
1079      ? `${agentAdvancedBadges.length} configured`
1080      : 'Defaults only'
1081  
1082    const toggleAgent = (id: string) => {
1083      setDelegationTargetMode('selected')
1084      setDelegationTargetAgentIds((prev) => {
1085        const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
1086        if (next.length === 0) {
1087          setDelegationTargetMode('all')
1088        }
1089        return next
1090      })
1091    }
1092  
1093    const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
1094  
1095    return (
1096      <>
1097      <BottomSheet open={open} onClose={onClose} wide>
1098        <div className="mb-8 pr-14 sm:pr-20">
1099          <div className="min-w-0">
1100            <div className="mb-2 flex flex-wrap items-center gap-2">
1101              <h2 className="font-display text-[28px] font-700 tracking-[-0.03em]">
1102                {editing ? 'Edit Agent' : 'New Agent'}
1103              </h2>
1104              <span className={`rounded-[999px] px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] ${
1105                disabled
1106                  ? 'border border-amber-400/20 bg-amber-400/[0.08] text-amber-300'
1107                  : 'border border-emerald-400/20 bg-emerald-400/[0.08] text-emerald-300'
1108              }`}>
1109                {disabled ? 'Disabled' : 'Enabled'}
1110              </span>
1111            </div>
1112            <p className="text-[14px] text-text-3">Set up an agent with sensible defaults, then expand advanced settings if you need deeper control.</p>
1113          </div>
1114        </div>
1115  
1116        <SectionCard
1117          title="Basics"
1118          description="Start with the core identity and description users will see first."
1119        >
1120        <div className="mb-8">
1121          <SectionLabel>Name</SectionLabel>
1122          <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
1123        </div>
1124  
1125        <div className="mb-8">
1126          <SectionLabel>Avatar</SectionLabel>
1127          <div className="flex flex-col gap-3">
1128            <div className="flex items-center gap-4">
1129              <div className="relative group shrink-0">
1130                <AgentAvatar seed={avatarUrl ? null : (avatarSeed || null)} avatarUrl={avatarUrl} name={name || 'A'} size={64} />
1131                <label className="absolute inset-0 rounded-full flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
1132                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1133                    <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
1134                    <circle cx="12" cy="13" r="4" />
1135                  </svg>
1136                  <input
1137                    type="file"
1138                    accept="image/*"
1139                    className="hidden"
1140                    onChange={async (e) => {
1141                      const file = e.target.files?.[0]
1142                      if (!file) return
1143                      setUploading(true)
1144                      try {
1145                        const res = await fetch('/api/upload', {
1146                          method: 'POST',
1147                          headers: { 'x-filename': file.name },
1148                          body: await file.arrayBuffer(),
1149                        })
1150                        const data = await res.json()
1151                        if (data.url) {
1152                          setAvatarUrl(data.url)
1153                          setAvatarSeed('')
1154                          toast.success('Avatar image uploaded')
1155                        }
1156                      } catch {
1157                        toast.error('Failed to upload image')
1158                      } finally {
1159                        setUploading(false)
1160                        e.target.value = ''
1161                      }
1162                    }}
1163                  />
1164                </label>
1165              </div>
1166              <div className="flex flex-col gap-1.5 flex-1 min-w-0">
1167                {avatarUrl && (
1168                  <button
1169                    type="button"
1170                    onClick={() => {
1171                      setAvatarUrl(null)
1172                      if (!avatarSeed) setAvatarSeed(Math.random().toString(36).slice(2, 10))
1173                    }}
1174                    className="text-[11px] text-text-3 hover:text-red-400 transition-colors self-start cursor-pointer"
1175                  >
1176                    Remove custom image
1177                  </button>
1178                )}
1179                {uploading && <span className="text-[11px] text-text-3">Uploading...</span>}
1180              </div>
1181            </div>
1182            <div className="flex items-center gap-3">
1183              <input
1184                type="text"
1185                value={avatarSeed}
1186                onChange={(e) => { setAvatarSeed(e.target.value); setAvatarUrl(null) }}
1187                placeholder="Avatar seed (any text)"
1188                className={inputClass}
1189                style={{ fontFamily: 'inherit', flex: 1 }}
1190              />
1191              <button
1192                type="button"
1193                onClick={() => { setAvatarSeed(Math.random().toString(36).slice(2, 10)); setAvatarUrl(null) }}
1194                className="inline-flex items-center gap-1.5 px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] hover:text-text-2 active:scale-95 shrink-0"
1195                style={{ fontFamily: 'inherit' }}
1196                title="Shuffle avatar"
1197              >
1198                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
1199                  <rect x="4" y="4" width="16" height="16" rx="2" />
1200                  <circle cx="9" cy="9" r="1" fill="currentColor" />
1201                  <circle cx="15" cy="15" r="1" fill="currentColor" />
1202                </svg>
1203                Shuffle
1204              </button>
1205            </div>
1206          </div>
1207        </div>
1208  
1209        <div className="mb-8">
1210          <SectionLabel>Description</SectionLabel>
1211          <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
1212        </div>
1213        </SectionCard>
1214  
1215        <SectionCard
1216          title="Model & Connection"
1217          description="Choose how this agent connects to a model, then verify the setup before saving."
1218        >
1219        <div className="mb-8">
1220          <div className="flex items-center justify-between gap-3 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-3">
1221            <div>
1222              <p className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3">Runtime</p>
1223              <p className="mt-1 text-[14px] font-600 text-text">{openclawEnabled ? 'OpenClaw gateway' : 'Direct provider connection'}</p>
1224            </div>
1225            <div className="flex items-center gap-3">
1226              <label className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3">OpenClaw</label>
1227              <button
1228                type="button"
1229                onClick={() => {
1230                  if (!openclawEnabled) {
1231                    setOpenclawEnabled(true)
1232                    setProvider('openclaw')
1233                    setModel('default')
1234                    if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
1235                  } else {
1236                    setOpenclawEnabled(false)
1237                    const first = agentSelectableProviders[0]?.id || 'claude-cli'
1238                    setProvider(first)
1239                    setModel('')
1240                    setApiEndpoint(null)
1241                    setCredentialId(null)
1242                    setGatewayProfileId(null)
1243                    setTestStatus('idle')
1244                    setTestMessage('')
1245                    setTestErrorCode(null)
1246                  }
1247                }}
1248                className={`relative h-6 w-11 rounded-full border-none transition-colors duration-200 ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1249              >
1250                <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${openclawEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1251              </button>
1252            </div>
1253          </div>
1254        </div>
1255        {/* OpenClaw Gateway Fields */}
1256        {openclawEnabled && (
1257          <div className="mb-8 space-y-5">
1258            {openclawGatewayProfiles.length > 0 && (
1259              <div>
1260                <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway Profile</label>
1261                <select
1262                  value={gatewayProfileId || ''}
1263                  onChange={(e) => applyGatewayProfileSelection(e.target.value || null)}
1264                  className={inputClass}
1265                >
1266                  <option value="">Custom endpoint</option>
1267                  {openclawGatewayProfiles.map((gateway) => (
1268                    <option key={gateway.id} value={gateway.id}>
1269                      {gateway.name}{gateway.isDefault ? ' (default)' : ''}
1270                    </option>
1271                  ))}
1272                </select>
1273              </div>
1274            )}
1275            {/* Connection fields */}
1276            <div className="space-y-4">
1277              <div>
1278                <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway URL</label>
1279                <input
1280                  type="text"
1281                  value={apiEndpoint || ''}
1282                  onChange={(e) => setApiEndpoint(e.target.value || null)}
1283                  placeholder="http://localhost:18789"
1284                  className={inputClass}
1285                  style={{ fontFamily: 'inherit' }}
1286                />
1287              </div>
1288              <div>
1289                <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway Token</label>
1290                {openclawCredentials.length > 0 && !addingKey ? (
1291                  <div className="flex gap-2">
1292                    <select value={credentialId || ''} onChange={(e) => {
1293                      if (e.target.value === '__add__') {
1294                        setAddingKey(true)
1295                        setNewKeyName('')
1296                        setNewKeyValue('')
1297                      } else {
1298                        setCredentialId(e.target.value || null)
1299                      }
1300                    }} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
1301                      <option value="">No token (auth disabled)</option>
1302                      {openclawCredentials.map((c) => (
1303                        <option key={c.id} value={c.id}>{c.name}</option>
1304                      ))}
1305                      <option value="__add__">+ Add new token...</option>
1306                    </select>
1307                    <button
1308                      type="button"
1309                      onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
1310                      className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
1311                    >
1312                      + New
1313                    </button>
1314                  </div>
1315                ) : (
1316                  <div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/10">
1317                    <input
1318                      type="text"
1319                      value={newKeyName}
1320                      onChange={(e) => setNewKeyName(e.target.value)}
1321                      placeholder="Label (e.g. Local gateway)"
1322                      className={inputClass}
1323                      style={{ fontFamily: 'inherit' }}
1324                    />
1325                    <input
1326                      type="password"
1327                      value={newKeyValue}
1328                      onChange={(e) => setNewKeyValue(e.target.value)}
1329                      placeholder="Paste gateway token..."
1330                      className={inputClass}
1331                      style={{ fontFamily: 'inherit' }}
1332                    />
1333                    <div className="flex gap-2 justify-end">
1334                      {openclawCredentials.length > 0 && (
1335                        <button type="button" onClick={() => setAddingKey(false)} className="px-3 py-1.5 text-[12px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>Cancel</button>
1336                      )}
1337                      <button
1338                        type="button"
1339                        disabled={savingKey || !newKeyValue.trim()}
1340                        onClick={async () => {
1341                          setSavingKey(true)
1342                          try {
1343                            const cred = await api<{ id: string }>('POST', '/credentials', { provider: 'openclaw', name: newKeyName.trim() || 'OpenClaw token', apiKey: newKeyValue.trim() })
1344                            await loadCredentials()
1345                            setCredentialId(cred.id)
1346                            setAddingKey(false)
1347                            setNewKeyName('')
1348                            setNewKeyValue('')
1349                          } catch (err: unknown) { toast.error(`Failed to save: ${errorMessage(err)}`) }
1350                          finally { setSavingKey(false) }
1351                        }}
1352                        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"
1353                        style={{ fontFamily: 'inherit' }}
1354                      >
1355                        {savingKey ? 'Saving...' : 'Save Token'}
1356                      </button>
1357                    </div>
1358                  </div>
1359                )}
1360              </div>
1361            </div>
1362  
1363            {/* Insecure connection warning */}
1364            {(() => {
1365              const url = (apiEndpoint || '').trim().toLowerCase()
1366              const isRemote = url && !/localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url)
1367              const isSecure = /^(https|wss):\/\//i.test(url)
1368              if (isRemote && !isSecure) return (
1369                <div className="px-3 py-2.5 rounded-[10px] bg-[#fbbf24]/[0.06] border border-[#fbbf24]/20">
1370                  <p className="text-[13px] text-[#fbbf24] leading-[1.5]">
1371                    Unencrypted connection. Use HTTPS or an SSH tunnel for production.
1372                  </p>
1373                </div>
1374              )
1375              return null
1376            })()}
1377  
1378            {/* Status feedback — single unified block */}
1379            {testStatus === 'pass' && (
1380              <div className="p-4 rounded-[12px] bg-emerald-500/[0.06] border border-emerald-500/15 space-y-2">
1381                <div className="flex items-center gap-2">
1382                  <StatusDot status="online" />
1383                  <p className="text-[14px] text-emerald-400 font-600">Connected</p>
1384                </div>
1385                <p className="text-[13px] text-text-2/80 leading-[1.6]">Gateway is reachable and this device is paired. Tools and models are managed by the OpenClaw instance.</p>
1386              </div>
1387            )}
1388            {testStatus === 'fail' && (
1389              <div className="p-4 rounded-[12px] border space-y-3"
1390                style={{
1391                  background: testErrorCode === 'PAIRING_REQUIRED' ? 'rgba(34,197,94,0.04)' : 'rgba(var(--accent-bright-rgb,120,100,255),0.06)',
1392                  borderColor: testErrorCode === 'PAIRING_REQUIRED' ? 'rgba(34,197,94,0.2)' : 'rgba(var(--accent-bright-rgb,120,100,255),0.15)',
1393                }}
1394              >
1395                {testErrorCode === 'PAIRING_REQUIRED' ? (<>
1396                  <div className="flex items-center gap-2">
1397                    <StatusDot status="online" pulse />
1398                    <p className="text-[14px] text-[#22c55e] font-600">Awaiting Approval</p>
1399                  </div>
1400                  <p className="text-[13px] text-text-2/80 leading-[1.6]">
1401                    This device is pending approval on your gateway. Go to <span className="text-text-2 font-500">Nodes</span>, approve the device{(testDeviceId || openclawDeviceId) ? <> (<code className="text-[12px] font-mono text-text-2/70">{(testDeviceId || openclawDeviceId)!.slice(0, 12)}...</code>)</> : null}, then click <span className="text-text-2 font-500">Retry Connection</span>.
1402                  </p>
1403                  <a
1404                    href={(() => { const ep = (apiEndpoint || 'http://localhost:18789').replace(/\/+$/, ''); return /^https?:\/\//i.test(ep) ? ep : `http://${ep}` })()}
1405                    target="_blank"
1406                    rel="noopener noreferrer"
1407                    className="inline-flex items-center gap-1.5 mt-2 px-4 py-2 rounded-[10px] bg-white/[0.06] border border-white/[0.1] text-[13px] text-text-2 font-500 hover:bg-white/[0.1] transition-colors"
1408                  >
1409                    Approve in Dashboard →
1410                  </a>
1411                </>) : testErrorCode === 'DEVICE_AUTH_INVALID' ? (<>
1412                  <p className="text-[14px] text-accent-bright font-600">Device Not Paired</p>
1413                  <p className="text-[13px] text-text-2/80 leading-[1.6]">
1414                    The gateway doesn&apos;t recognize this device. Go to <span className="text-text-2 font-500">Nodes</span>, and add or approve this device{(testDeviceId || openclawDeviceId) ? <> (<code className="text-[12px] font-mono text-text-2/70">{(testDeviceId || openclawDeviceId)!.slice(0, 12)}...</code>)</> : null}.
1415                  </p>
1416                  <a
1417                    href={(() => { const ep = (apiEndpoint || 'http://localhost:18789').replace(/\/+$/, ''); return /^https?:\/\//i.test(ep) ? ep : `http://${ep}` })()}
1418                    target="_blank"
1419                    rel="noopener noreferrer"
1420                    className="inline-flex items-center gap-1.5 mt-2 px-4 py-2 rounded-[10px] bg-white/[0.06] border border-white/[0.1] text-[13px] text-text-2 font-500 hover:bg-white/[0.1] transition-colors"
1421                  >
1422                    Approve in Dashboard →
1423                  </a>
1424                </>) : testErrorCode === 'AUTH_TOKEN_MISSING' ? (<>
1425                  <p className="text-[14px] text-accent-bright font-600">Token Required</p>
1426                  <p className="text-[13px] text-text-2/80 leading-[1.6]">
1427                    This gateway requires an auth token. Add one above and try again.
1428                  </p>
1429                </>) : testErrorCode === 'AUTH_TOKEN_INVALID' ? (<>
1430                  <p className="text-[14px] text-accent-bright font-600">Invalid Token</p>
1431                  <p className="text-[13px] text-text-2/80 leading-[1.6]">
1432                    The gateway rejected this token. Check that it matches the one configured on your OpenClaw instance.
1433                  </p>
1434                </>) : (<>
1435                  <p className="text-[14px] text-accent-bright font-600">Connection Failed</p>
1436                  <p className="text-[13px] text-text-2/80 leading-[1.6]">
1437                    {testMessage || 'Could not reach the gateway. Check the URL, token, and that the gateway is running.'}
1438                  </p>
1439                </>)}
1440                {/* Device ID footer — always shown on failure for debugging */}
1441                {(testDeviceId || openclawDeviceId) && testErrorCode !== 'AUTH_TOKEN_MISSING' && testErrorCode !== 'AUTH_TOKEN_INVALID' && (
1442                  <div className="pt-2 border-t border-white/[0.04]">
1443                    <p className="text-[12px] text-text-3/70 flex items-center gap-1.5">
1444                      Device <code className="font-mono text-text-2/70 select-all">{(testDeviceId || openclawDeviceId)}</code>
1445                      <button
1446                        type="button"
1447                        onClick={() => {
1448                          void copyTextToClipboard((testDeviceId || openclawDeviceId)!).then((copiedId) => {
1449                            if (!copiedId) return
1450                            setConfigCopied(true)
1451                            setTimeout(() => setConfigCopied(false), 2000)
1452                          })
1453                        }}
1454                        className="text-[12px] text-text-3/60 hover:text-text-3/80 transition-colors cursor-pointer bg-transparent border-none"
1455                      >
1456                        {configCopied ? 'copied' : 'copy'}
1457                      </button>
1458                    </p>
1459                  </div>
1460                )}
1461              </div>
1462            )}
1463          </div>
1464        )}
1465  
1466        {!openclawEnabled && <div className="mb-8">
1467          <SectionLabel>Provider</SectionLabel>
1468          <div className="grid grid-cols-3 gap-3">
1469            {agentSelectableProviders.map((p) => {
1470              const nextCredentials = resolveAgentSelectableProviderCredentials(p.id, credentials, providerConfigs)
1471              const isConnected = !p.requiresApiKey || nextCredentials.length > 0
1472              return (
1473                <button
1474                  key={p.id}
1475                  onClick={() => {
1476                    setProvider(p.id)
1477                    if (!nextCredentials.some((item) => item.id === credentialId)) {
1478                      setCredentialId(nextCredentials[0]?.id || null)
1479                    }
1480                    setGatewayProfileId(null)
1481                  }}
1482                  className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
1483                    active:scale-[0.97] text-[14px] font-600 border
1484                    ${provider === p.id
1485                      ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1486                      : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
1487                  style={{ fontFamily: 'inherit' }}
1488                >
1489                  {isConnected && (
1490                    <span className="absolute top-2 right-2 w-2 h-2 rounded-full bg-emerald-400" />
1491                  )}
1492                  {p.name}
1493                </button>
1494              )
1495            })}
1496          </div>
1497        </div>}
1498  
1499        {!openclawEnabled && currentProvider && currentProvider.models.length > 0 && (
1500          <div className="mb-8">
1501            <SectionLabel>Model</SectionLabel>
1502            <ModelCombobox
1503              providerId={currentProvider.id}
1504              value={model}
1505              onChange={setModel}
1506              models={currentProvider.models}
1507              defaultModels={currentProvider.defaultModels}
1508              credentialId={credentialId}
1509              apiEndpoint={apiEndpoint}
1510              ollamaMode={provider === 'ollama' ? ollamaMode : null}
1511              supportsDiscovery={currentProvider.supportsModelDiscovery}
1512              className={`${inputClass} cursor-pointer`}
1513            />
1514          </div>
1515        )}
1516  
1517        {/* OpenClaw manages its own models — no selector needed */}
1518  
1519        {/* Ollama Mode Toggle */}
1520        {!openclawEnabled && provider === 'ollama' && (
1521          <div className="mb-8">
1522            <SectionLabel>Mode</SectionLabel>
1523            <div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
1524              {(['local', 'cloud'] as const).map((mode) => (
1525                <button
1526                  key={mode}
1527                  onClick={() => {
1528                    setOllamaMode(mode)
1529                    if (mode === 'local') {
1530                      setApiEndpoint('http://localhost:11434')
1531                      setCredentialId(null)
1532                    } else {
1533                      setApiEndpoint(null)
1534                      if (providerCredentials.length > 0) setCredentialId(providerCredentials[0].id)
1535                    }
1536                  }}
1537                  className={`flex-1 py-3 rounded-[12px] text-center cursor-pointer transition-all duration-200
1538                    text-[14px] font-600 capitalize
1539                    ${ollamaMode === mode
1540                      ? 'bg-accent-soft text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
1541                      : 'bg-transparent text-text-3 hover:text-text-2'}`}
1542                  style={{ fontFamily: 'inherit' }}
1543                >
1544                  {mode}
1545                </button>
1546              ))}
1547            </div>
1548          </div>
1549        )}
1550  
1551        {!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && (
1552          <div className="mb-8">
1553            <SectionLabel>API Key{currentProvider?.optionalApiKey && !currentProvider?.requiresApiKey && <span className="normal-case tracking-normal font-normal text-text-3"> (optional)</span>}</SectionLabel>
1554            {providerCredentials.length > 0 && !addingKey ? (
1555              <div className="flex gap-2">
1556                <select value={credentialId || ''} onChange={(e) => {
1557                  if (e.target.value === '__add__') {
1558                    setAddingKey(true)
1559                    setNewKeyName('')
1560                    setNewKeyValue('')
1561                  } else {
1562                    setCredentialId(e.target.value || null)
1563                  }
1564                }} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
1565                  <option value="">Select a key...</option>
1566                  {providerCredentials.map((c) => (
1567                    <option key={c.id} value={c.id}>{c.name}</option>
1568                  ))}
1569                  <option value="__add__">+ Add new key...</option>
1570                </select>
1571                <button
1572                  type="button"
1573                  onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
1574                  className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
1575                >
1576                  + New
1577                </button>
1578              </div>
1579            ) : (
1580              <div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
1581                <input
1582                  type="text"
1583                  value={newKeyName}
1584                  onChange={(e) => setNewKeyName(e.target.value)}
1585                  placeholder="Key name (optional)"
1586                  className={inputClass}
1587                  style={{ fontFamily: 'inherit' }}
1588                />
1589                <input
1590                  type="password"
1591                  value={newKeyValue}
1592                  onChange={(e) => setNewKeyValue(e.target.value)}
1593                  placeholder="Paste API key..."
1594                  className={inputClass}
1595                  style={{ fontFamily: 'inherit' }}
1596                />
1597                <div className="flex gap-2 justify-end">
1598                  {providerCredentials.length > 0 && (
1599                    <button type="button" onClick={() => setAddingKey(false)} className="px-3 py-1.5 text-[12px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>Cancel</button>
1600                  )}
1601                  <button
1602                    type="button"
1603                    disabled={savingKey || !newKeyValue.trim()}
1604                        onClick={async () => {
1605                          setSavingKey(true)
1606                          try {
1607                            const cred = await api<{ id: string }>('POST', '/credentials', { provider, name: newKeyName.trim() || `${provider} key`, apiKey: newKeyValue.trim() })
1608                            await loadCredentials()
1609                            setCredentialId(cred.id)
1610                            const synced = await syncLiveProviderModels(provider, cred.id, apiEndpoint, ollamaMode, true).catch(() => null)
1611                            setAddingKey(false)
1612                            setNewKeyName('')
1613                            setNewKeyValue('')
1614                            if (synced?.models.length) {
1615                              toast.success(`Key saved. Synced ${synced.models.length} model${synced.models.length === 1 ? '' : 's'}.`)
1616                            } else {
1617                              toast.success('Key saved')
1618                            }
1619                          } catch (err: unknown) { toast.error(`Failed to save: ${errorMessage(err)}`) }
1620                          finally { setSavingKey(false) }
1621                        }}
1622                    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"
1623                    style={{ fontFamily: 'inherit' }}
1624                  >
1625                    {savingKey ? 'Saving...' : 'Save Key'}
1626                  </button>
1627                </div>
1628              </div>
1629            )}
1630          </div>
1631        )}
1632  
1633        {/* Fallback Credentials */}
1634        {!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && providerCredentials.length > 1 && (
1635          <div className="mb-8">
1636            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1637              Fallback Keys <span className="normal-case tracking-normal font-normal text-text-3">(for auto-failover)</span>
1638            </label>
1639            <p className="text-[12px] text-text-3/60 mb-3">If the primary key fails (rate limit, auth error), these keys will be tried in order.</p>
1640            <div className="flex flex-wrap gap-2">
1641              {providerCredentials.filter((c) => c.id !== credentialId).map((c) => {
1642                const active = fallbackCredentialIds.includes(c.id)
1643                return (
1644                  <button
1645                    key={c.id}
1646                    onClick={() => setFallbackCredentialIds((prev) => active ? prev.filter((x) => x !== c.id) : [...prev, c.id])}
1647                    className={`px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
1648                      ${active
1649                        ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1650                        : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1651                    style={{ fontFamily: 'inherit' }}
1652                  >
1653                    {c.name}
1654                  </button>
1655                )
1656              })}
1657            </div>
1658          </div>
1659        )}
1660  
1661        {(currentProvider?.requiresEndpoint || currentProvider?.optionalEndpoint) && (provider !== 'ollama' || ollamaMode === 'local') && (
1662          <div className="mb-8">
1663            <SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : provider === 'hermes' ? 'Hermes API Endpoint' : 'Endpoint'}</SectionLabel>
1664            <input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
1665            {provider === 'openclaw' && (
1666              <p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
1667            )}
1668            {provider === 'hermes' && (
1669              <p className="text-[13px] text-text-3/70 mt-2">Point this at the Hermes API server, usually <code className="text-text-2">http://127.0.0.1:8642/v1</code>.</p>
1670            )}
1671          </div>
1672        )}
1673  
1674        </SectionCard>
1675  
1676        <SectionCard
1677          title="Instructions"
1678          description="Keep the agent's personality and core prompt visible and easy to edit."
1679        >
1680          <div className="mb-8">
1681            <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1682              Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
1683              <HintTip text="The agent's voice and tone — how it talks, not what it knows" />
1684              {soul !== soulInitial && soulSaveState === 'idle' && (
1685                <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-amber-400 font-600">
1686                  <StatusDot status="warning" size="sm" />
1687                  Unsaved
1688                </span>
1689              )}
1690              {soulSaveState === 'saved' && (
1691                <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-emerald-400 font-600">
1692                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
1693                  Saved
1694                </span>
1695              )}
1696            </label>
1697            <div className="mb-3 flex flex-wrap items-center gap-2">
1698              <p className="text-[12px] text-text-3/60">Define the agent&apos;s voice, tone, and personality. Injected before the system prompt.</p>
1699              <button
1700                type="button"
1701                onClick={() => setSoul(randomSoul())}
1702                className="inline-flex items-center gap-1.5 shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors"
1703                style={{ fontFamily: 'inherit' }}
1704                title="Randomize personality"
1705              >
1706                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
1707                  <rect x="4" y="4" width="16" height="16" rx="2" />
1708                  <circle cx="9" cy="9" r="1" fill="currentColor" />
1709                  <circle cx="15" cy="15" r="1" fill="currentColor" />
1710                </svg>
1711                Shuffle
1712              </button>
1713              <button
1714                type="button"
1715                onClick={() => setSoulLibraryOpen(true)}
1716                className="shrink-0 px-2 py-1 rounded-[8px] border border-accent-bright/20 bg-accent-soft text-[11px] text-accent-bright hover:brightness-110 cursor-pointer transition-colors"
1717                style={{ fontFamily: 'inherit' }}
1718              >
1719                Browse Library
1720              </button>
1721              <button onClick={() => soulFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
1722              <input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
1723            </div>
1724            <textarea
1725              value={soul}
1726              onChange={(e) => setSoul(e.target.value)}
1727              placeholder="e.g. You speak concisely and directly. You have a dry sense of humor. You always back claims with data."
1728              rows={3}
1729              className={`${inputClass} resize-y min-h-[80px]`}
1730              style={{ fontFamily: 'inherit' }}
1731            />
1732          </div>
1733  
1734          {provider !== 'openclaw' ? (
1735            <div className="mb-1">
1736              <div className="mb-3 flex items-center gap-2">
1737                <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt <HintTip text="Instructions that tell the agent what it can do, what tools to use, and how to behave" /></label>
1738                <button onClick={() => promptFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
1739                <input ref={promptFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSystemPrompt)} className="hidden" />
1740              </div>
1741              <textarea
1742                value={systemPrompt}
1743                onChange={(e) => setSystemPrompt(e.target.value)}
1744                placeholder="You are an expert..."
1745                rows={6}
1746                className={`${inputClass} resize-y min-h-[140px]`}
1747                style={{ fontFamily: 'inherit' }}
1748              />
1749            </div>
1750          ) : (
1751            <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4 text-[13px] leading-[1.6] text-text-3">
1752              OpenClaw agents rely on the gateway runtime for tool execution and node routing. Expand advanced settings if you need continuity, voice, or heartbeat overrides.
1753            </div>
1754          )}
1755        </SectionCard>
1756  
1757        {(!WORKER_ONLY_PROVIDER_IDS.has(provider) || isOrchestratorProviderEligible(provider)) && (
1758        <SectionCard
1759          title="Role & Autonomy"
1760          description="Define how this agent operates in the swarm."
1761        >
1762          {/* --- Role subsection --- */}
1763          {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
1764            <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4 mb-4">
1765              <div className="flex items-center gap-2 mb-3">
1766                <SectionLabel>Role</SectionLabel>
1767                <HintTip text="Coordinators automatically receive a list of available agents and can decompose complex goals, delegate to specialists, and synthesize results." />
1768              </div>
1769              <div className="flex gap-2 mb-3">
1770                {(['worker', 'coordinator'] as const).map((r) => (
1771                  <button
1772                    key={r}
1773                    type="button"
1774                    onClick={() => {
1775                      setRole(r)
1776                      if (r === 'coordinator') setDelegationEnabled(true)
1777                    }}
1778                    className={`px-4 py-1.5 rounded-[8px] text-[13px] font-display font-500 transition-all duration-200
1779                      ${role === r
1780                        ? 'bg-accent-bright text-white'
1781                        : 'bg-white/[0.06] text-text-3 hover:bg-white/[0.10]'}`}
1782                  >
1783                    {r === 'worker' ? 'Worker' : 'Coordinator'}
1784                  </button>
1785                ))}
1786              </div>
1787              <p className="text-[12px] text-text-3/75">
1788                {role === 'coordinator'
1789                  ? 'Breaks down complex goals, delegates to specialists, and synthesizes results'
1790                  : 'Executes tasks when prompted by users or other agents'}
1791              </p>
1792  
1793              {/* Delegation toggle */}
1794              <div className="mt-4">
1795                <label className="flex items-center gap-3 cursor-pointer">
1796                  <div
1797                    onClick={() => {
1798                      if (role !== 'coordinator') setDelegationEnabled((current) => !current)
1799                    }}
1800                    className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1801                      ${canDelegateToAgents ? 'bg-accent-bright' : 'bg-white/[0.08]'}
1802                      ${role === 'coordinator' ? 'opacity-60' : ''}`}
1803                  >
1804                    <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1805                      ${canDelegateToAgents ? 'left-[22px]' : 'left-0.5'}`} />
1806                  </div>
1807                  <span className="font-display text-[14px] font-600 text-text-2">Can Delegate</span>
1808                  <span className="text-[12px] text-text-3">
1809                    {role === 'coordinator' ? 'Always on for coordinators' : 'Route work to specialized agents'}
1810                  </span>
1811                </label>
1812              </div>
1813  
1814              {/* Delegation targets */}
1815              {canDelegateToAgents && agentOptions.length > 0 && (
1816                <div className="mt-4">
1817                  <SectionLabel>Allowed Delegate Agents</SectionLabel>
1818                  <AgentPickerList
1819                    agents={agentOptions}
1820                    selected={delegationTargetMode === 'all' ? [] : delegationTargetAgentIds}
1821                    onSelect={(id) => toggleAgent(id)}
1822                    noneOption={{
1823                      label: 'All Agents',
1824                      onSelect: () => {
1825                        setDelegationTargetMode('all')
1826                        setDelegationTargetAgentIds([])
1827                      },
1828                    }}
1829                  />
1830                </div>
1831              )}
1832            </div>
1833          )}
1834  
1835          {/* --- Orchestrator subsection --- */}
1836          {isOrchestratorProviderEligible(provider) && (
1837            <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
1838              <div className="flex items-center justify-between gap-4">
1839                <div className="min-w-0">
1840                  <p className="text-[14px] font-600 text-text">Orchestrator Mode</p>
1841                  <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
1842                    Wakes on a schedule to autonomously review platform state and take action.
1843                  </p>
1844                </div>
1845                <button
1846                  type="button"
1847                  onClick={() => setOrchestratorEnabled((current) => !current)}
1848                  className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${orchestratorEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1849                  aria-pressed={orchestratorEnabled}
1850                >
1851                  <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${orchestratorEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1852                </button>
1853              </div>
1854  
1855              {orchestratorEnabled && (
1856                <div className="mt-4 space-y-4">
1857                  <div>
1858                    <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1859                      Mission
1860                    </label>
1861                    <textarea
1862                      value={orchestratorMission}
1863                      onChange={(e) => setOrchestratorMission(e.target.value)}
1864                      placeholder="Describe the orchestrator's mission — what should it manage, optimize, or oversee?"
1865                      rows={3}
1866                      className={`${inputClass} resize-y min-h-[84px]`}
1867                      style={{ fontFamily: 'inherit' }}
1868                    />
1869                  </div>
1870  
1871                  <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
1872                    <div>
1873                      <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1874                        Wake Interval
1875                      </label>
1876                      <input
1877                        type="text"
1878                        value={orchestratorWakeInterval}
1879                        onChange={(e) => setOrchestratorWakeInterval(e.target.value)}
1880                        placeholder="5m"
1881                        className={inputClass}
1882                        style={{ fontFamily: 'inherit' }}
1883                      />
1884                    </div>
1885                    <div>
1886                      <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1887                        Governance
1888                      </label>
1889                      <select
1890                        value={orchestratorGovernance}
1891                        onChange={(e) => setOrchestratorGovernance(e.target.value as typeof orchestratorGovernance)}
1892                        className={inputClass}
1893                        style={{ fontFamily: 'inherit' }}
1894                      >
1895                        <option value="autonomous">Autonomous</option>
1896                        <option value="approval-required">Approval Required</option>
1897                        <option value="notify-only">Notify Only</option>
1898                      </select>
1899                    </div>
1900                    <div>
1901                      <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1902                        Max Cycles/Day
1903                      </label>
1904                      <input
1905                        type="number"
1906                        value={orchestratorMaxCyclesPerDay}
1907                        onChange={(e) => setOrchestratorMaxCyclesPerDay(e.target.value)}
1908                        placeholder="No limit"
1909                        min={1}
1910                        className={inputClass}
1911                        style={{ fontFamily: 'inherit' }}
1912                      />
1913                    </div>
1914                  </div>
1915                </div>
1916              )}
1917            </div>
1918          )}
1919        </SectionCard>
1920        )}
1921  
1922        {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
1923        <SectionCard
1924          title="Behavior"
1925          description="Keep the core autonomy switch visible. Expert heartbeat controls stay in advanced settings."
1926        >
1927          <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
1928            <div className="min-w-0">
1929              <p className="text-[14px] font-600 text-text">Heartbeat</p>
1930              <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
1931                Keep this agent alive in the background for proactive work and scheduled follow-through.
1932              </p>
1933            </div>
1934            <button
1935              type="button"
1936              onClick={() => setHeartbeatEnabled((current) => !current)}
1937              className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${heartbeatEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1938              aria-pressed={heartbeatEnabled}
1939            >
1940              <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${heartbeatEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1941            </button>
1942          </div>
1943          <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4 mt-3">
1944            <div className="min-w-0">
1945              <div className="flex items-center gap-2">
1946                <p className="text-[14px] font-600 text-text">Dreaming</p>
1947                <HintTip text="When enabled, this agent consolidates and optimizes its memories during idle periods" />
1948              </div>
1949              <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
1950                Consolidate, decay, and reflect on memories when the agent is idle.
1951              </p>
1952            </div>
1953            <button
1954              type="button"
1955              onClick={() => setDreamEnabled((current) => !current)}
1956              className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${dreamEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1957              aria-pressed={dreamEnabled}
1958            >
1959              <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${dreamEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1960            </button>
1961          </div>
1962          {dreamEnabled && (
1963            <div className="mt-3 rounded-[14px] border border-white/[0.04] bg-white/[0.01] px-4 py-4 space-y-3">
1964              <div>
1965                <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
1966                  Cooldown (minutes) <HintTip text="Minimum minutes between dream cycles" />
1967                </label>
1968                <input
1969                  type="number"
1970                  value={dreamCooldownMinutes}
1971                  onChange={(e) => setDreamCooldownMinutes(e.target.value)}
1972                  min={1}
1973                  placeholder="360"
1974                  className={inputClass}
1975                  style={{ fontFamily: 'inherit' }}
1976                />
1977              </div>
1978              <label className="flex items-center gap-3 cursor-pointer">
1979                <div
1980                  onClick={() => setDreamTier2Enabled((current) => !current)}
1981                  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${dreamTier2Enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1982                >
1983                  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${dreamTier2Enabled ? 'left-[22px]' : 'left-0.5'}`} />
1984                </div>
1985                <span className="flex items-center gap-2 text-[13px] text-text-2">
1986                  Tier 2 Reflection <HintTip text="Use the agent's LLM to reflect on memories and produce consolidated insights" />
1987                </span>
1988              </label>
1989            </div>
1990          )}
1991        </SectionCard>
1992        )}
1993  
1994        {editing && (
1995          <SectionCard
1996            title="Social Network"
1997            description="SwarmFeed integration — let this agent post and engage on the social feed."
1998          >
1999            <AgentSocialSettings agent={editing} />
2000          </SectionCard>
2001        )}
2002  
2003        {editing && (
2004          <SectionCard
2005            title="Marketplace"
2006            description="SwarmDock integration — list this agent on the AI marketplace to accept tasks and earn USDC."
2007          >
2008            <AgentMarketplaceSettings agent={editing} />
2009          </SectionCard>
2010        )}
2011  
2012        {(!WORKER_ONLY_PROVIDER_IDS.has(provider) || MCP_INJECTION_PROVIDER_IDS.has(provider)) && (
2013        <AdvancedSettingsSection
2014          open={showAdvancedSettings}
2015          onToggle={() => setShowAdvancedSettings((current) => !current)}
2016          summary={advancedSummary}
2017          badges={agentAdvancedBadges}
2018        >
2019        {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (<>
2020        <SectionCard
2021          title="Context & Tool Access"
2022          description="Control how many tools are described in this agent's system prompt. Scoped (default) keeps the agent focused and saves ~3 k input tokens per turn; Universal gives it visibility into every built-in tool."
2023          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2024        >
2025        <div className="space-y-3">
2026          <label className="flex items-center gap-3 cursor-pointer">
2027            <div
2028              onClick={() => setToolAccessMode((current) => current === 'universal' ? 'scoped' : 'universal')}
2029              className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${toolAccessMode === 'universal' ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
2030            >
2031              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${toolAccessMode === 'universal' ? 'left-[22px]' : 'left-0.5'}`} />
2032            </div>
2033            <span className="text-[13px] text-text-2">Universal tool access</span>
2034            <HintTip text="Off (default, recommended): the agent only sees tools enabled in its Tools list. On: every built-in tool is described in the system prompt. Turn on only for coordinator agents that need visibility across every possible downstream tool, or temporarily for debugging." />
2035          </label>
2036          <p className="text-[12px] text-text-3/70 pl-[56px] -mt-1">
2037            {toolAccessMode === 'universal'
2038              ? 'Full tool universe is injected into the prompt. Costs ~3 k more input tokens per turn.'
2039              : 'Only the tools enabled above are visible to the agent — this is the focused default.'}
2040          </p>
2041        </div>
2042        </SectionCard>
2043  
2044        <SectionCard
2045          title="Voice & Autonomy"
2046          description="Tune voice and the detailed heartbeat behavior for this agent."
2047          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2048        >
2049        <div className="mb-8">
2050          <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2051            Voice &amp; Audio
2052          </label>
2053          {voiceControlsAvailable ? (
2054            <>
2055              <div className="grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_auto] gap-3">
2056                <input
2057                  type="text"
2058                  value={voiceId}
2059                  onChange={(e) => setVoiceId(e.target.value)}
2060                  placeholder="ElevenLabs voice ID"
2061                  className={inputClass}
2062                  style={{ fontFamily: 'inherit' }}
2063                />
2064                <button
2065                  type="button"
2066                  onClick={() => setVoiceId('')}
2067                  className="px-3 py-2.5 rounded-[10px] border border-white/[0.08] bg-transparent text-[12px] font-600 text-text-3 hover:bg-white/[0.04] hover:text-text-2 transition-all cursor-pointer"
2068                  style={{ fontFamily: 'inherit' }}
2069                >
2070                  Use global default
2071                </button>
2072              </div>
2073              <p className="mt-2 text-[12px] leading-[1.6] text-text-3/70">
2074                Current effective voice: <span className="text-text-2">{effectiveVoiceId}</span> · {effectiveVoiceSource}
2075                {!voicePlaybackEnabled && ' · Voice playback is disabled globally'}
2076              </p>
2077            </>
2078          ) : (
2079            <p className="text-[12px] leading-[1.6] text-text-3/70">
2080              ElevenLabs is not configured yet. Add a global API key in Settings to enable voice overrides here.
2081            </p>
2082          )}
2083        </div>
2084  
2085        <div>
2086          <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2087            Heartbeat Controls
2088          </label>
2089          <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
2090            <select
2091              value={heartbeatIntervalSec}
2092              onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
2093              className={inputClass}
2094              style={{ fontFamily: 'inherit' }}
2095            >
2096              <option value="">Use default interval</option>
2097              {HB_PRESETS.map((preset) => (
2098                <option key={preset} value={preset}>{formatHbDuration(preset)}</option>
2099              ))}
2100            </select>
2101            <input
2102              type="text"
2103              value={heartbeatModel}
2104              onChange={(e) => setHeartbeatModel(e.target.value)}
2105              placeholder="Heartbeat model override"
2106              className={inputClass}
2107              style={{ fontFamily: 'inherit' }}
2108            />
2109          </div>
2110          <textarea
2111            value={heartbeatPrompt}
2112            onChange={(e) => setHeartbeatPrompt(e.target.value)}
2113            placeholder="Optional custom heartbeat prompt"
2114            rows={3}
2115            className={`${inputClass} resize-y min-h-[84px]`}
2116            style={{ fontFamily: 'inherit' }}
2117          />
2118        </div>
2119        </SectionCard>
2120  
2121        <SectionCard
2122          title="Memory & Intelligence"
2123          description="Reasoning depth, memory defaults, and drafting behavior."
2124          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2125        >
2126        <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
2127          <select value={thinkingLevel} onChange={(e) => setThinkingLevel(e.target.value as typeof thinkingLevel)} className={inputClass}>
2128            <option value="">Default thinking</option>
2129            <option value="minimal">Minimal</option>
2130            <option value="low">Low</option>
2131            <option value="medium">Medium</option>
2132            <option value="high">High</option>
2133          </select>
2134          <select value={memoryScopeMode} onChange={(e) => setMemoryScopeMode(e.target.value as typeof memoryScopeMode)} className={inputClass}>
2135            <option value="auto">Auto memory scope</option>
2136            <option value="all">All</option>
2137            <option value="global">Global</option>
2138            <option value="agent">Agent</option>
2139            <option value="session">Session</option>
2140            <option value="project">Project</option>
2141          </select>
2142          <select value={memoryTierPreference} onChange={(e) => setMemoryTierPreference(e.target.value as typeof memoryTierPreference)} className={inputClass}>
2143            <option value="blended">Blended tiering</option>
2144            <option value="working">Working memory</option>
2145            <option value="durable">Durable memory</option>
2146            <option value="archive">Archive memory</option>
2147          </select>
2148        </div>
2149        <div className="space-y-3">
2150          <label className="flex items-center gap-3 cursor-pointer">
2151            <div
2152              onClick={() => setProactiveMemory((current) => !current)}
2153              className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${proactiveMemory ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
2154            >
2155              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${proactiveMemory ? 'left-[22px]' : 'left-0.5'}`} />
2156            </div>
2157            <span className="text-[13px] text-text-2">Use proactive recall before each run</span>
2158          </label>
2159          <label className="flex items-center gap-3 cursor-pointer">
2160            <div
2161              onClick={() => setAutoDraftSkillSuggestions((current) => !current)}
2162              className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${autoDraftSkillSuggestions ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
2163            >
2164              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${autoDraftSkillSuggestions ? 'left-[22px]' : 'left-0.5'}`} />
2165            </div>
2166            <span className="text-[13px] text-text-2">Auto-draft conversation skills</span>
2167          </label>
2168        </div>
2169        </SectionCard>
2170  
2171        <SectionCard
2172          title="Continuity"
2173          description="Stable identity, relationship context, and session reset policy."
2174          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2175        >
2176        <div className="mb-8">
2177          <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2178            Identity Continuity <HintTip text="Seeds the agent's continuity state so session memory can preserve a stable persona and relationship context." />
2179          </label>
2180          <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
2181            <input type="text" value={identityPersonaLabel} onChange={(e) => setIdentityPersonaLabel(e.target.value)} placeholder="Persona label" className={inputClass} style={{ fontFamily: 'inherit' }} />
2182            <input type="text" value={identityToneStyle} onChange={(e) => setIdentityToneStyle(e.target.value)} placeholder="Tone style" className={inputClass} style={{ fontFamily: 'inherit' }} />
2183          </div>
2184          <div className="grid grid-cols-1 gap-3">
2185            <textarea value={identitySelfSummary} onChange={(e) => setIdentitySelfSummary(e.target.value)} placeholder="How this agent should summarize itself across sessions." rows={3} className={`${inputClass} resize-y min-h-[84px]`} style={{ fontFamily: 'inherit' }} />
2186            <textarea value={identityRelationshipSummary} onChange={(e) => setIdentityRelationshipSummary(e.target.value)} placeholder="Relationship framing or standing context the agent should keep in mind." rows={3} className={`${inputClass} resize-y min-h-[84px]`} style={{ fontFamily: 'inherit' }} />
2187            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2188              <textarea value={identityBoundariesText} onChange={(e) => setIdentityBoundariesText(e.target.value)} placeholder="Boundaries, one per line." rows={4} className={`${inputClass} resize-y min-h-[108px]`} style={{ fontFamily: 'inherit' }} />
2189              <textarea value={identityContinuityNotesText} onChange={(e) => setIdentityContinuityNotesText(e.target.value)} placeholder="Continuity notes, one per line." rows={4} className={`${inputClass} resize-y min-h-[108px]`} style={{ fontFamily: 'inherit' }} />
2190            </div>
2191          </div>
2192          <p className="mt-2 text-[12px] leading-[1.5] text-text-3/60">
2193            Use one line per item. Boundaries are stable guardrails; continuity notes are recurring relationship or project context worth carrying across sessions.
2194          </p>
2195        </div>
2196  
2197        <div>
2198          <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2199            Session Reset Policy <HintTip text="Controls when this agent's sessions are considered stale and should be refreshed." />
2200          </label>
2201          <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
2202            <select value={sessionResetMode} onChange={(e) => setSessionResetMode(e.target.value as typeof sessionResetMode)} className={inputClass} style={{ fontFamily: 'inherit' }}>
2203              <option value="">Inherit global default</option>
2204              <option value="idle">Idle</option>
2205              <option value="daily">Daily</option>
2206              <option value="isolated">Isolated (fresh context per run)</option>
2207            </select>
2208            <input type="number" min={0} value={sessionIdleTimeoutSec} onChange={(e) => setSessionIdleTimeoutSec(e.target.value)} placeholder="Idle timeout in seconds" className={inputClass} style={{ fontFamily: 'inherit' }} />
2209          </div>
2210          <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
2211            <input type="number" min={0} value={sessionMaxAgeSec} onChange={(e) => setSessionMaxAgeSec(e.target.value)} placeholder="Max age in seconds" className={inputClass} style={{ fontFamily: 'inherit' }} />
2212            <input type="text" value={sessionDailyResetAt} onChange={(e) => setSessionDailyResetAt(e.target.value)} placeholder="Daily reset time (HH:MM)" className={inputClass} style={{ fontFamily: 'inherit' }} />
2213            <input type="text" value={sessionResetTimezone} onChange={(e) => setSessionResetTimezone(e.target.value)} placeholder="Timezone (optional)" className={inputClass} style={{ fontFamily: 'inherit' }} />
2214          </div>
2215        </div>
2216        </SectionCard>
2217  
2218        <SectionCard
2219          title="Routing & Infrastructure"
2220          description="Project binding, filesystem access, and other deeper runtime controls."
2221          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2222        >
2223        {Object.keys(projects).length > 0 && (
2224          <div className="mb-8">
2225            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Project</label>
2226            <select value={projectId || ''} onChange={(e) => setProjectId(e.target.value || undefined)} className={inputClass} style={{ fontFamily: 'inherit' }}>
2227              <option value="">No project</option>
2228              {Object.values(projects).map((project) => (
2229                <option key={project.id} value={project.id}>{project.name}</option>
2230              ))}
2231            </select>
2232          </div>
2233        )}
2234        {openclawEnabled && (
2235          <div className="mb-8">
2236            <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2237              Gateway Preferences <HintTip text="When multiple OpenClaw gateways are available, prefer matching tags or deployment templates before falling back to the default route." />
2238            </label>
2239            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2240              <input
2241                type="text"
2242                value={preferredGatewayTagsText}
2243                onChange={(e) => setPreferredGatewayTagsText(e.target.value)}
2244                placeholder="gpu, local, research"
2245                className={inputClass}
2246              />
2247              <select value={preferredGatewayUseCase} onChange={(e) => setPreferredGatewayUseCase(e.target.value)} className={inputClass}>
2248                <option value="">Any OpenClaw template</option>
2249                <option value="local-dev">Local Dev</option>
2250                <option value="single-vps">Single VPS</option>
2251                <option value="private-tailnet">Private Tailnet</option>
2252                <option value="browser-heavy">Browser Heavy</option>
2253                <option value="team-control">Team Control</option>
2254              </select>
2255            </div>
2256            <p className="text-[11px] text-text-3/70 mt-2">
2257              These preferences bias scheduling toward matching OpenClaw control planes without hard-locking the agent to one gateway.
2258            </p>
2259          </div>
2260        )}
2261        <div className="mb-8">
2262          <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2263            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." />
2264          </label>
2265          <div className="flex items-center gap-3 mb-3">
2266            <select value={routingStrategy} onChange={(e) => setRoutingStrategy(e.target.value as AgentRoutingStrategy)} className={inputClass}>
2267              <option value="single">Single route</option>
2268              <option value="balanced">Balanced</option>
2269              <option value="economy">Economy</option>
2270              <option value="premium">Premium</option>
2271              <option value="reasoning">Reasoning</option>
2272            </select>
2273            <button
2274              type="button"
2275              onClick={addRoutingTargetFromCurrent}
2276              className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-700 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
2277            >
2278              + Add Current Route
2279            </button>
2280          </div>
2281          <div className="space-y-3">
2282            {routingTargets.map((target, index) => {
2283              const targetCredentials = resolveAgentSelectableProviderCredentials(target.provider, credentials, providerConfigs)
2284              return (
2285                <div key={target.id} className="p-4 rounded-[12px] border border-white/[0.08] bg-white/[0.02] space-y-3">
2286                  <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2287                    <input
2288                      value={target.label || ''}
2289                      onChange={(e) => updateRoutingTarget(target.id, { label: e.target.value })}
2290                      placeholder={`Route ${index + 1} label`}
2291                      className={inputClass}
2292                    />
2293                    <select value={target.role || 'backup'} onChange={(e) => updateRoutingTarget(target.id, { role: e.target.value as AgentRoutingTarget['role'] })} className={inputClass}>
2294                      <option value="primary">Primary</option>
2295                      <option value="economy">Economy</option>
2296                      <option value="premium">Premium</option>
2297                      <option value="reasoning">Reasoning</option>
2298                      <option value="backup">Backup</option>
2299                    </select>
2300                  </div>
2301                  <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2302                    <select
2303                      value={target.provider}
2304                      onChange={(e) => {
2305                        const nextProviderId = e.target.value
2306                        const nextCredentials = resolveAgentSelectableProviderCredentials(nextProviderId, credentials, providerConfigs)
2307                        updateRoutingTarget(target.id, {
2308                          provider: nextProviderId,
2309                          credentialId: nextCredentials[0]?.id || null,
2310                          gatewayProfileId: nextProviderId === 'openclaw' ? target.gatewayProfileId : null,
2311                          ollamaMode: nextProviderId === 'ollama'
2312                          ? resolveStoredOllamaMode({
2313                            ollamaMode: target.ollamaMode ?? null,
2314                            apiEndpoint: target.apiEndpoint ?? null,
2315                          })
2316                          : null,
2317                        })
2318                      }}
2319                      className={inputClass}
2320                    >
2321                      {agentSelectableProviders.map((item) => (
2322                        <option key={item.id} value={item.id}>{item.name}</option>
2323                      ))}
2324                    </select>
2325                    <input
2326                      value={target.model}
2327                      onChange={(e) => updateRoutingTarget(target.id, { model: e.target.value })}
2328                      placeholder="Model"
2329                      className={inputClass}
2330                    />
2331                  </div>
2332                  {target.provider === 'openclaw' && openclawGatewayProfiles.length > 0 && (
2333                    <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
2334                      <select
2335                        value={target.gatewayProfileId || ''}
2336                        onChange={(e) => {
2337                          const nextId = e.target.value || null
2338                          const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
2339                          updateRoutingTarget(target.id, {
2340                            gatewayProfileId: nextId,
2341                            apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
2342                            credentialId: gateway?.credentialId || target.credentialId || null,
2343                            model: target.model || 'default',
2344                          })
2345                        }}
2346                        className={inputClass}
2347                      >
2348                        <option value="">Custom OpenClaw endpoint</option>
2349                        {openclawGatewayProfiles.map((gateway) => (
2350                          <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
2351                        ))}
2352                      </select>
2353                      <input
2354                        value={formatGatewayTagList(target.preferredGatewayTags)}
2355                        onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayTags: parseGatewayTagList(e.target.value) })}
2356                        placeholder="Prefer tags"
2357                        className={inputClass}
2358                      />
2359                      <select
2360                        value={target.preferredGatewayUseCase || ''}
2361                        onChange={(e) => updateRoutingTarget(target.id, { preferredGatewayUseCase: e.target.value || null })}
2362                        className={inputClass}
2363                      >
2364                        <option value="">Any OpenClaw template</option>
2365                        <option value="local-dev">Local Dev</option>
2366                        <option value="single-vps">Single VPS</option>
2367                        <option value="private-tailnet">Private Tailnet</option>
2368                        <option value="browser-heavy">Browser Heavy</option>
2369                        <option value="team-control">Team Control</option>
2370                      </select>
2371                    </div>
2372                  )}
2373                  <div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
2374                    <input
2375                      value={target.apiEndpoint || ''}
2376                      onChange={(e) => updateRoutingTarget(target.id, { apiEndpoint: e.target.value || null })}
2377                      placeholder="Endpoint (optional)"
2378                      className={`${inputClass} font-mono text-[14px]`}
2379                    />
2380                    <select value={target.credentialId || ''} onChange={(e) => updateRoutingTarget(target.id, { credentialId: e.target.value || null })} className={inputClass}>
2381                      <option value="">No key</option>
2382                      {targetCredentials.map((item) => (
2383                        <option key={item.id} value={item.id}>{item.name}</option>
2384                      ))}
2385                    </select>
2386                  </div>
2387                  <div className="flex justify-end">
2388                    <button type="button" onClick={() => removeRoutingTarget(target.id)} className="px-3 py-1.5 rounded-[8px] border border-red-400/20 bg-red-400/[0.06] text-[12px] font-700 text-red-300 hover:bg-red-400/[0.1] transition-all cursor-pointer">
2389                      Remove Route
2390                    </button>
2391                  </div>
2392                </div>
2393              )
2394            })}
2395          </div>
2396          {routingTargets.length === 0 && (
2397            <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>
2398          )}
2399        </div>
2400        <div>
2401          <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Filesystem Access</label>
2402          <select
2403            value={filesystemScope}
2404            onChange={(e) => setFilesystemScope(e.target.value as 'workspace' | 'machine')}
2405            className="w-full h-10 px-3 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[14px] text-text-2"
2406          >
2407            <option value="workspace">Workspace only</option>
2408            <option value="machine">Full machine</option>
2409          </select>
2410          {filesystemScope === 'machine' && (
2411            <p className="mt-2 text-[12px] text-amber-400/80">Agent can access any file your user account can reach. Sensitive paths (.ssh, .env, .gnupg) are blocked by default.</p>
2412          )}
2413        </div>
2414        </SectionCard>
2415  
2416        <SectionCard
2417          title="Safety & Limits"
2418          description="Enable safeguards, recovery, and spend limits without crowding the main setup flow."
2419          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2420        >
2421        <div className="space-y-3 mb-6">
2422          <label className="flex items-center gap-3 cursor-pointer">
2423            <div onClick={() => setDisabled((current) => !current)} className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${disabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}>
2424              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${disabled ? 'left-[22px]' : 'left-0.5'}`} />
2425            </div>
2426            <span className="text-[13px] text-text-2">Disable this agent</span>
2427          </label>
2428          <label className="flex items-center gap-3 cursor-pointer">
2429            <div onClick={() => setAutoRecovery((current) => !current)} className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${autoRecovery ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}>
2430              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${autoRecovery ? 'left-[22px]' : 'left-0.5'}`} />
2431            </div>
2432            <span className="text-[13px] text-text-2">Guardian auto-recovery</span>
2433          </label>
2434        </div>
2435  
2436        <div className="mb-4">
2437          <label className="flex items-center gap-3 cursor-pointer">
2438            <div onClick={() => setBudgetEnabled((current) => !current)} className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${budgetEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}>
2439              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${budgetEnabled ? 'left-[22px]' : 'left-0.5'}`} />
2440            </div>
2441            <span className="text-[13px] text-text-2">Spend limits</span>
2442          </label>
2443        </div>
2444        {budgetEnabled && (
2445          <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
2446            <input type="number" min={0} step="0.01" value={hourlyBudget} onChange={(e) => setHourlyBudget(e.target.value)} placeholder="Hourly" className={inputClass} style={{ fontFamily: 'inherit' }} />
2447            <input type="number" min={0} step="0.01" value={dailyBudget} onChange={(e) => setDailyBudget(e.target.value)} placeholder="Daily" className={inputClass} style={{ fontFamily: 'inherit' }} />
2448            <input type="number" min={0} step="0.01" value={monthlyBudget} onChange={(e) => setMonthlyBudget(e.target.value)} placeholder="Monthly" className={inputClass} style={{ fontFamily: 'inherit' }} />
2449            <select value={budgetAction} onChange={(e) => setBudgetAction(e.target.value as typeof budgetAction)} className={inputClass} style={{ fontFamily: 'inherit' }}>
2450              <option value="warn">Warn</option>
2451              <option value="block">Block</option>
2452            </select>
2453          </div>
2454        )}
2455        </SectionCard>
2456        </>)}
2457  
2458        <SectionCard
2459          title="Tools & Skills"
2460          description="Enable tool families, pin preferred skills, and connect MCP tools for this agent."
2461          className="mb-6 border-white/[0.05] bg-white/[0.01]"
2462        >
2463        {/* Tools — hidden for providers that manage capabilities outside LangGraph */}
2464        {!hasNativeCapabilities && (
2465          <div className="mb-8">
2466            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Tools</label>
2467            <p className="text-[12px] text-text-3/60 mb-3">Enable built-in tool families for this agent.</p>
2468            <div className="space-y-3">
2469              {AVAILABLE_TOOLS
2470                .map((t) => {
2471                  const extensionDisabled = !!t.extensionId && !!enabledExtensionIds && !enabledExtensionIds.has(t.extensionId)
2472                  return (
2473                    <label key={t.id} className={`flex items-center gap-3 ${extensionDisabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}`} title={extensionDisabled ? 'Enable in Extensions page' : undefined}>
2474                      <div
2475                        onClick={() => !extensionDisabled && setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
2476                        className={`w-11 h-6 rounded-full transition-all duration-200 relative shrink-0
2477                          ${extensionDisabled ? 'bg-white/[0.04] cursor-not-allowed' : tools.includes(t.id) ? 'bg-accent-bright cursor-pointer' : 'bg-white/[0.08] cursor-pointer'}`}
2478                      >
2479                        <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
2480                          ${tools.includes(t.id) && !extensionDisabled ? 'left-[22px]' : 'left-0.5'}`} />
2481                      </div>
2482                      <span className={`font-display text-[14px] font-600 ${extensionDisabled ? 'text-text-3/40' : 'text-text-2'}`}>{t.label}</span>
2483                      <span className={`text-[12px] ${extensionDisabled ? 'text-text-3/30' : 'text-text-3'}`}>
2484                        {extensionDisabled ? 'Enable in Extensions page' : t.description}
2485                      </span>
2486                    </label>
2487                  )
2488                })}
2489            </div>
2490          </div>
2491        )}
2492  
2493        {/* Filesystem Access */}
2494        <div className="mb-8">
2495          <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Filesystem Access</label>
2496          <select
2497            value={filesystemScope}
2498            onChange={(e) => setFilesystemScope(e.target.value as 'workspace' | 'machine')}
2499            className="w-full h-10 px-3 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[14px] text-text-2"
2500          >
2501            <option value="workspace">Workspace only</option>
2502            <option value="machine">Full machine</option>
2503          </select>
2504          {filesystemScope === 'machine' && (
2505            <p className="mt-2 text-[12px] text-amber-400/80">Agent can access any file your user account can reach. Sensitive paths (.ssh, .env, .gnupg) are blocked by default.</p>
2506          )}
2507        </div>
2508  
2509        {/* Platform — hidden for providers that manage capabilities outside LangGraph */}
2510        {!hasNativeCapabilities && (
2511          <div className="mb-8">
2512            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Platform Tools</label>
2513            <p className="text-[12px] text-text-3/60 mb-3">Allow this agent to manage platform resources directly.</p>
2514            <div className="space-y-3">
2515              {PLATFORM_TOOLS
2516                .map((t) => {
2517                  const extensionDisabled = !!t.extensionId && !!enabledExtensionIds && !enabledExtensionIds.has(t.extensionId)
2518                  return (
2519                    <label key={t.id} className={`flex items-center gap-3 ${extensionDisabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}`} title={extensionDisabled ? 'Enable in Extensions page' : undefined}>
2520                      <div
2521                        onClick={() => !extensionDisabled && setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
2522                        className={`w-11 h-6 rounded-full transition-all duration-200 relative shrink-0
2523                          ${extensionDisabled ? 'bg-white/[0.04] cursor-not-allowed' : tools.includes(t.id) ? 'bg-accent-bright cursor-pointer' : 'bg-white/[0.08] cursor-pointer'}`}
2524                      >
2525                        <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
2526                          ${tools.includes(t.id) && !extensionDisabled ? 'left-[22px]' : 'left-0.5'}`} />
2527                      </div>
2528                      <span className={`font-display text-[14px] font-600 ${extensionDisabled ? 'text-text-3/40' : 'text-text-2'}`}>{t.label}</span>
2529                      <span className={`text-[12px] ${extensionDisabled ? 'text-text-3/30' : 'text-text-3'}`}>
2530                        {extensionDisabled ? 'Enable in Extensions page' : t.description}
2531                      </span>
2532                    </label>
2533                  )
2534                })}
2535            </div>
2536          </div>
2537        )}
2538  
2539        {/* Native capability provider note — not shown for OpenClaw (covered in connection status) */}
2540        {hasNativeCapabilities && !openclawEnabled && (
2541          <div className="mb-8 p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
2542            <p className="text-[13px] text-text-3">
2543              {provider === 'claude-cli'
2544                ? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
2545                : provider === 'codex-cli'
2546                  ? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.). Skills and MCP servers assigned below will be injected at runtime.'
2547                  : provider === 'opencode-cli'
2548                    ? 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration is needed.'
2549                    : provider === 'gemini-cli'
2550                      ? 'Gemini CLI uses its own built-in tools and runtime — SwarmClaw does not inject local platform tools for it.'
2551                      : provider === 'copilot-cli'
2552                        ? 'GitHub Copilot CLI uses its own built-in tools and runtime. Skills and MCP servers assigned below will be injected at runtime.'
2553                        : provider === 'droid-cli'
2554                          ? 'Factory Droid CLI uses its own built-in tools and autonomy controls — SwarmClaw does not inject local platform tools for it.'
2555                          : provider === 'cursor-cli'
2556                          ? 'Cursor Agent CLI runs with its own native tool/runtime layer — SwarmClaw sends prompts directly without injecting local platform tools.'
2557                          : provider === 'qwen-code-cli'
2558                            ? 'Qwen Code CLI uses its own native tools and runtime — SwarmClaw does not inject local platform tools for it.'
2559                            : provider === 'goose'
2560                              ? 'Goose manages its own runtime, tools, and extensions — SwarmClaw sends prompts directly instead of injecting local platform tools.'
2561                              : provider === 'hermes'
2562                                ? 'Hermes Agent runs behind its own API server and tool runtime. SwarmClaw sends prompts to Hermes directly instead of injecting local platform tools.'
2563                                : 'This provider manages its own native capabilities and runtime.'}
2564            </p>
2565          </div>
2566        )}
2567  
2568        {/* Skills — discovered from ~/.claude/skills/ */}
2569        {provider === 'claude-cli' && (
2570          <div className="mb-8">
2571            <div className="flex items-center justify-between mb-2">
2572              <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
2573                Pinned Claude Skills <span className="normal-case tracking-normal font-normal text-text-3">(from ~/.claude/skills/)</span>
2574              </label>
2575              <button
2576                onClick={loadClaudeSkills}
2577                disabled={claudeSkillsLoading}
2578                className="text-[11px] text-text-3 hover:text-accent-bright transition-colors cursor-pointer bg-transparent border-none flex items-center gap-1"
2579                style={{ fontFamily: 'inherit' }}
2580                title="Refresh skills from ~/.claude/skills/"
2581              >
2582                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
2583                  className={claudeSkillsLoading ? 'animate-spin' : ''}>
2584                  <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
2585                </svg>
2586                Refresh
2587              </button>
2588            </div>
2589            <p className="text-[12px] text-text-3/60 mb-3">Optional preference list. Pinned Claude skills are called out explicitly when this agent is delegated work.</p>
2590            {claudeSkills.length > 0 ? (
2591              <div className="flex flex-wrap gap-2">
2592                {claudeSkills.map((s) => {
2593                  const active = skills.includes(s.id)
2594                  return (
2595                    <button
2596                      key={s.id}
2597                      onClick={() => setSkills((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
2598                      className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
2599                        ${active
2600                          ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
2601                          : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
2602                      style={{ fontFamily: 'inherit' }}
2603                      title={s.description}
2604                    >
2605                      {s.name}
2606                    </button>
2607                  )
2608                })}
2609              </div>
2610            ) : (
2611              <p className="text-[12px] text-text-3/70">No skills found in ~/.claude/skills/</p>
2612            )}
2613          </div>
2614        )}
2615  
2616        {/* Dynamic Skills from Skills Manager */}
2617        {Object.keys(dynamicSkills).length > 0 && (
2618          <div className="mb-8">
2619            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2620              Pinned Skills <span className="normal-case tracking-normal font-normal text-text-3">(from Skills manager)</span>
2621            </label>
2622            <p className="text-[12px] text-text-3/60 mb-3">All ready local skills are discoverable by default. Pin skills here only when they should stay in this agent&apos;s prompt as always-on guidance.</p>
2623            <div className="flex flex-wrap gap-2">
2624              {Object.values(dynamicSkills).map((s) => {
2625                const active = skillIds.includes(s.id)
2626                return (
2627                  <button
2628                    key={s.id}
2629                    onClick={() => setSkillIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
2630                    className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
2631                      ${active
2632                        ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
2633                        : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
2634                    style={{ fontFamily: 'inherit' }}
2635                    title={s.description || s.filename}
2636                  >
2637                    {s.name}
2638                  </button>
2639                )
2640              })}
2641            </div>
2642          </div>
2643        )}
2644  
2645        {/* MCP Servers */}
2646        {Object.keys(mcpServers).length > 0 && (
2647          <div className="mb-8">
2648            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2649              MCP Servers
2650            </label>
2651            <p className="text-[12px] text-text-3/60 mb-3">Connect external tool servers to this agent via MCP.</p>
2652            <div className="flex flex-wrap gap-2">
2653              {Object.values(mcpServers).map((s) => {
2654                const active = mcpServerIds.includes(s.id)
2655                return (
2656                  <button
2657                    key={s.id}
2658                    onClick={() => setMcpServerIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
2659                    className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
2660                      ${active
2661                        ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
2662                        : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
2663                    style={{ fontFamily: 'inherit' }}
2664                    title={`${s.transport} — ${s.command || s.url || ''}`}
2665                  >
2666                    {s.name}
2667                  </button>
2668                )
2669              })}
2670            </div>
2671          </div>
2672        )}
2673  
2674        {/* MCP Tools — per-tool enable/disable toggles */}
2675        {mcpServerIds.length > 0 && Object.keys(mcpTools).length > 0 && (
2676          <div className="mb-8">
2677            <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
2678              MCP Tools
2679            </label>
2680            <p className="text-[12px] text-text-3/60 mb-3">
2681              Toggle individual tools from connected MCP servers.{mcpToolsLoading ? ' Loading…' : ''}
2682            </p>
2683            <div className="space-y-4">
2684              {mcpServerIds.map((serverId) => {
2685                const server = mcpServers[serverId]
2686                const serverTools = mcpTools[serverId]
2687                if (!server || !serverTools?.length) return null
2688                const safeName = server.name.replace(/[^a-zA-Z0-9_]/g, '_')
2689                return (
2690                  <div key={serverId}>
2691                    <p className="text-[12px] font-600 text-text-3 mb-2">{server.name}</p>
2692                    <div className="space-y-3">
2693                      {serverTools.map((t) => {
2694                        const fullName = `mcp_${safeName}_${t.name}`
2695                        const enabled = !mcpDisabledTools.includes(fullName)
2696                        return (
2697                          <label key={fullName} className="flex items-center gap-3 cursor-pointer">
2698                            <div
2699                              onClick={() => setMcpDisabledTools((prev) =>
2700                                enabled ? [...prev, fullName] : prev.filter((x) => x !== fullName)
2701                              )}
2702                              className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
2703                                ${enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
2704                            >
2705                              <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
2706                                ${enabled ? 'left-[22px]' : 'left-0.5'}`} />
2707                            </div>
2708                            <span className="font-display text-[14px] font-600 text-text-2">{t.name}</span>
2709                            <span className="text-[12px] text-text-3 truncate">{t.description}</span>
2710                          </label>
2711                        )
2712                      })}
2713                    </div>
2714                  </div>
2715                )
2716              })}
2717            </div>
2718          </div>
2719        )}
2720  
2721        <div className="mb-2">
2722          <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Capabilities</label>
2723          <p className="text-[12px] text-text-3/60 mb-3">Optional tags that describe what this agent is especially good at.</p>
2724          {capabilities.length > 0 && (
2725            <div className="mb-3 flex flex-wrap gap-2">
2726              {capabilities.map((capability) => (
2727                <span key={capability} className="inline-flex items-center gap-1.5 rounded-[8px] border border-accent-bright/20 bg-accent-soft/20 px-3 py-1 text-[12px] text-accent-bright">
2728                  {capability}
2729                  <button
2730                    type="button"
2731                    onClick={() => setCapabilities((current) => current.filter((entry) => entry !== capability))}
2732                    className="bg-transparent border-none text-accent-bright/70 hover:text-accent-bright cursor-pointer"
2733                  >
2734                    ×
2735                  </button>
2736                </span>
2737              ))}
2738            </div>
2739          )}
2740          <div className="flex gap-2">
2741            <input
2742              type="text"
2743              value={capInput}
2744              onChange={(e) => setCapInput(e.target.value)}
2745              onKeyDown={(e) => {
2746                if (e.key !== 'Enter') return
2747                e.preventDefault()
2748                const next = capInput.trim()
2749                if (!next || capabilities.includes(next)) return
2750                setCapabilities((current) => [...current, next])
2751                setCapInput('')
2752              }}
2753              placeholder="Add a capability tag"
2754              className={inputClass}
2755              style={{ fontFamily: 'inherit' }}
2756            />
2757            <button
2758              type="button"
2759              onClick={() => {
2760                const next = capInput.trim()
2761                if (!next || capabilities.includes(next)) return
2762                setCapabilities((current) => [...current, next])
2763                setCapInput('')
2764              }}
2765              className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-700 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
2766            >
2767              Add
2768            </button>
2769          </div>
2770        </div>
2771        </SectionCard>
2772  
2773        <SectionCard
2774          title="Utilities"
2775          description="Import and export agents."
2776          className="mb-0 border-white/[0.05] bg-white/[0.01]"
2777        >
2778        <div className="flex flex-wrap gap-3">
2779          {editing ? (
2780            <button
2781              type="button"
2782              onClick={handleExport}
2783              className="px-4 py-2.5 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all"
2784              style={{ fontFamily: 'inherit' }}
2785            >
2786              Export agent
2787            </button>
2788          ) : (
2789            <button
2790              type="button"
2791              onClick={() => importFileRef.current?.click()}
2792              className="px-4 py-2.5 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all"
2793              style={{ fontFamily: 'inherit' }}
2794            >
2795              Import agent
2796            </button>
2797          )}
2798        </div>
2799        </SectionCard>
2800        </AdvancedSettingsSection>
2801        )}
2802  
2803        {/* Provider key warning */}
2804        {providerNeedsKey && (
2805          <div className="mb-4 p-3 rounded-[12px] bg-amber-500/[0.08] border border-amber-500/20">
2806            <p className="text-[13px] text-amber-400">
2807              Add an API key for {currentProvider?.name || provider} above before creating this agent.
2808            </p>
2809          </div>
2810        )}
2811  
2812        {/* Test connection result (hidden for OpenClaw — inline status block handles it) */}
2813        {!openclawEnabled && testStatus === 'fail' && (
2814          <div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
2815            <p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
2816          </div>
2817        )}
2818        {!openclawEnabled && testStatus === 'pass' && (
2819          <div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
2820            <p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
2821          </div>
2822        )}
2823  
2824        {/* Import file input (hidden) */}
2825        <input ref={importFileRef} type="file" accept=".json" onChange={handleImport} className="hidden" />
2826  
2827        <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
2828          {editing && (
2829            <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
2830              Delete
2831            </button>
2832          )}
2833          <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
2834            Cancel
2835          </button>
2836          <button
2837            onClick={handleTestAndSave}
2838            disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || saving || (!openclawEnabled && testStatus === 'pass')}
2839            className={`flex-1 py-3.5 rounded-[14px] border-none text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-60 transition-all hover:brightness-110
2840              ${testStatus === 'pass' ? 'bg-emerald-600 shadow-[0_4px_20px_rgba(16,185,129,0.25)]' : 'bg-accent-bright shadow-[0_4px_20px_rgba(99,102,241,0.25)]'}`}
2841            style={{ fontFamily: 'inherit' }}
2842          >
2843            {openclawEnabled
2844              ? (testStatus === 'testing' ? 'Connecting...'
2845                : testStatus === 'pass' ? (saving ? 'Saving...' : 'Save')
2846                : testStatus === 'fail' && testErrorCode === 'PAIRING_REQUIRED' ? 'Retry Connection'
2847                : testStatus === 'fail' ? 'Retry'
2848                : 'Connect')
2849              : (testStatus === 'testing' ? 'Testing...' : testStatus === 'pass' ? (saving ? 'Saving...' : 'Connected!') : needsTest ? 'Test & Save' : editing ? 'Save' : 'Create')}
2850          </button>
2851        </div>
2852      </BottomSheet>
2853  
2854      <SoulLibraryPicker
2855        open={soulLibraryOpen}
2856        onClose={() => setSoulLibraryOpen(false)}
2857        onSelect={(s) => setSoul(s)}
2858      />
2859      </>
2860    )
2861  }