@swarmclawai/swarmclaw 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +56 -42
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +16 -35
  15. package/src/app/api/tts/stream/route.ts +14 -42
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +76 -24
  31. package/src/components/chat/chat-header.tsx +522 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +113 -8
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +84 -17
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +125 -14
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/connector-routing.test.ts +118 -1
  78. package/src/lib/server/connectors/discord.ts +31 -8
  79. package/src/lib/server/connectors/manager.ts +594 -16
  80. package/src/lib/server/connectors/media.ts +5 -0
  81. package/src/lib/server/connectors/telegram.ts +12 -2
  82. package/src/lib/server/connectors/types.ts +2 -0
  83. package/src/lib/server/connectors/whatsapp.ts +28 -2
  84. package/src/lib/server/elevenlabs.test.ts +60 -0
  85. package/src/lib/server/elevenlabs.ts +103 -0
  86. package/src/lib/server/heartbeat-service.ts +8 -1
  87. package/src/lib/server/main-agent-loop.ts +1 -1
  88. package/src/lib/server/memory-consolidation.ts +15 -2
  89. package/src/lib/server/memory-db.ts +134 -6
  90. package/src/lib/server/mime.ts +51 -0
  91. package/src/lib/server/openclaw-gateway.ts +2 -2
  92. package/src/lib/server/orchestrator-lg.ts +2 -0
  93. package/src/lib/server/orchestrator.ts +5 -2
  94. package/src/lib/server/playwright-proxy.mjs +2 -3
  95. package/src/lib/server/prompt-runtime-context.ts +53 -0
  96. package/src/lib/server/queue.ts +182 -8
  97. package/src/lib/server/session-tools/canvas.ts +67 -0
  98. package/src/lib/server/session-tools/connector.ts +583 -63
  99. package/src/lib/server/session-tools/crud.ts +21 -0
  100. package/src/lib/server/session-tools/delegate.ts +68 -4
  101. package/src/lib/server/session-tools/file.ts +26 -7
  102. package/src/lib/server/session-tools/git.ts +71 -0
  103. package/src/lib/server/session-tools/http.ts +57 -0
  104. package/src/lib/server/session-tools/index.ts +8 -0
  105. package/src/lib/server/session-tools/memory.ts +1 -0
  106. package/src/lib/server/session-tools/search-providers.ts +16 -8
  107. package/src/lib/server/session-tools/subagent.ts +106 -0
  108. package/src/lib/server/session-tools/web.ts +118 -8
  109. package/src/lib/server/stream-agent-chat.ts +39 -10
  110. package/src/lib/server/task-mention.ts +41 -0
  111. package/src/lib/sessions.ts +10 -0
  112. package/src/lib/soul-library.ts +103 -0
  113. package/src/lib/task-dedupe.ts +26 -0
  114. package/src/lib/tool-definitions.ts +2 -0
  115. package/src/lib/tts.ts +2 -2
  116. package/src/stores/use-app-store.ts +5 -1
  117. package/src/stores/use-chat-store.ts +65 -2
  118. package/src/types/index.ts +32 -2
@@ -14,8 +14,9 @@ import { AgentAvatar } from './agent-avatar'
14
14
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
15
15
  import { randomSoul } from '@/lib/soul-suggestions'
16
16
  import { SectionLabel } from '@/components/shared/section-label'
17
+ import { SoulLibraryPicker } from './soul-library-picker'
17
18
 
18
- const HB_PRESETS = [30, 60, 120, 300, 600, 1800, 3600] as const
19
+ const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
19
20
 
20
21
  function formatHbDuration(sec: number): string {
21
22
  if (sec >= 3600) {
@@ -63,6 +64,7 @@ export function AgentSheet() {
63
64
  const loadProviders = useAppStore((s) => s.loadProviders)
64
65
  const credentials = useAppStore((s) => s.credentials)
65
66
  const loadCredentials = useAppStore((s) => s.loadCredentials)
67
+ const appSettings = useAppStore((s) => s.appSettings)
66
68
  const dynamicSkills = useAppStore((s) => s.skills)
67
69
  const mcpServers = useAppStore((s) => s.mcpServers)
68
70
  const loadSkills = useAppStore((s) => s.loadSkills)
@@ -80,6 +82,8 @@ export function AgentSheet() {
80
82
  const [name, setName] = useState('')
81
83
  const [description, setDescription] = useState('')
82
84
  const [soul, setSoul] = useState('')
85
+ const [soulInitial, setSoulInitial] = useState('')
86
+ const [soulSaveState, setSoulSaveState] = useState<'idle' | 'saved'>('idle')
83
87
  const [systemPrompt, setSystemPrompt] = useState('')
84
88
  const [provider, setProvider] = useState<ProviderType>('claude-cli')
85
89
  const [model, setModel] = useState('')
@@ -95,7 +99,6 @@ export function AgentSheet() {
95
99
  const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
96
100
  const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
97
101
  const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
98
- // platformAssignScope is derived from isOrchestrator — no separate state needed
99
102
  const [capabilities, setCapabilities] = useState<string[]>([])
100
103
  const [capInput, setCapInput] = useState('')
101
104
  const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
@@ -103,6 +106,7 @@ export function AgentSheet() {
103
106
  const [projectId, setProjectId] = useState<string | undefined>(undefined)
104
107
  const [avatarSeed, setAvatarSeed] = useState('')
105
108
  const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
109
+ const [voiceId, setVoiceId] = useState('')
106
110
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
107
111
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
108
112
  const [heartbeatModel, setHeartbeatModel] = useState('')
@@ -121,6 +125,7 @@ export function AgentSheet() {
121
125
  const [configCopied, setConfigCopied] = useState(false)
122
126
 
123
127
  const soulFileRef = useRef<HTMLInputElement>(null)
128
+ const [soulLibraryOpen, setSoulLibraryOpen] = useState(false)
124
129
  const promptFileRef = useRef<HTMLInputElement>(null)
125
130
  const importFileRef = useRef<HTMLInputElement>(null)
126
131
 
@@ -157,6 +162,8 @@ export function AgentSheet() {
157
162
  setName(editing.name)
158
163
  setDescription(editing.description)
159
164
  setSoul(editing.soul || '')
165
+ setSoulInitial(editing.soul || '')
166
+ setSoulSaveState('idle')
160
167
  setSystemPrompt(editing.systemPrompt)
161
168
  setProvider(editing.provider)
162
169
  setModel(editing.model)
@@ -178,6 +185,7 @@ export function AgentSheet() {
178
185
  setProjectId(editing.projectId)
179
186
  setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
180
187
  setThinkingLevel(editing.thinkingLevel || '')
188
+ setVoiceId(editing.elevenLabsVoiceId || '')
181
189
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
182
190
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
183
191
  setHeartbeatModel(editing.heartbeatModel || '')
@@ -185,7 +193,10 @@ export function AgentSheet() {
185
193
  } else {
186
194
  setName('')
187
195
  setDescription('')
188
- setSoul(randomSoul())
196
+ const newSoul = randomSoul()
197
+ setSoul(newSoul)
198
+ setSoulInitial(newSoul)
199
+ setSoulSaveState('idle')
189
200
  setSystemPrompt('')
190
201
  setProvider('claude-cli')
191
202
  setModel('')
@@ -205,6 +216,7 @@ export function AgentSheet() {
205
216
  setProjectId(undefined)
206
217
  setAvatarSeed('')
207
218
  setThinkingLevel('')
219
+ setVoiceId('')
208
220
  setHeartbeatEnabled(false)
209
221
  setHeartbeatIntervalSec('')
210
222
  setHeartbeatModel('')
@@ -299,6 +311,7 @@ export function AgentSheet() {
299
311
  projectId: projectId || undefined,
300
312
  avatarSeed: avatarSeed.trim() || undefined,
301
313
  thinkingLevel: thinkingLevel || undefined,
314
+ elevenLabsVoiceId: voiceId.trim() || null,
302
315
  heartbeatEnabled,
303
316
  heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
304
317
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
@@ -313,6 +326,9 @@ export function AgentSheet() {
313
326
  toast.success('Agent created')
314
327
  }
315
328
  await loadAgents()
329
+ setSoulInitial(soul)
330
+ setSoulSaveState('saved')
331
+ setTimeout(() => setSoulSaveState('idle'), 1500)
316
332
  onClose()
317
333
  }
318
334
 
@@ -419,6 +435,7 @@ export function AgentSheet() {
419
435
  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"
420
436
 
421
437
  return (
438
+ <>
422
439
  <BottomSheet open={open} onClose={onClose} wide>
423
440
  <div className="mb-10 flex items-start justify-between">
424
441
  <div>
@@ -577,6 +594,24 @@ export function AgentSheet() {
577
594
  <p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
578
595
  </div>
579
596
 
597
+ {/* ElevenLabs Voice ID */}
598
+ {appSettings.elevenLabsEnabled && (
599
+ <div className="mb-8">
600
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
601
+ ElevenLabs Voice ID <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
602
+ </label>
603
+ <input
604
+ type="text"
605
+ value={voiceId}
606
+ onChange={(e) => setVoiceId(e.target.value)}
607
+ placeholder="Leave blank for global default"
608
+ className={inputClass}
609
+ style={{ fontFamily: 'inherit' }}
610
+ />
611
+ <p className="text-[11px] text-text-3/70 mt-1.5">Override the default voice for this agent. Leave blank to use the global default.</p>
612
+ </div>
613
+ )}
614
+
580
615
  {/* Heartbeat Configuration */}
581
616
  <div className="mb-8">
582
617
  <div className="flex items-center justify-between mb-3">
@@ -633,8 +668,20 @@ export function AgentSheet() {
633
668
 
634
669
  {provider !== 'openclaw' && (
635
670
  <div className="mb-8">
636
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
671
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
637
672
  Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
673
+ {soul !== soulInitial && soulSaveState === 'idle' && (
674
+ <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-amber-400 font-600">
675
+ <span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
676
+ Unsaved
677
+ </span>
678
+ )}
679
+ {soulSaveState === 'saved' && (
680
+ <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-emerald-400 font-600">
681
+ <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>
682
+ Saved
683
+ </span>
684
+ )}
638
685
  </label>
639
686
  <div className="flex items-center gap-2 mb-3">
640
687
  <p className="text-[12px] text-text-3/60">Define the agent&apos;s voice, tone, and personality. Injected before the system prompt.</p>
@@ -652,6 +699,14 @@ export function AgentSheet() {
652
699
  </svg>
653
700
  Shuffle
654
701
  </button>
702
+ <button
703
+ type="button"
704
+ onClick={() => setSoulLibraryOpen(true)}
705
+ 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"
706
+ style={{ fontFamily: 'inherit' }}
707
+ >
708
+ Browse Library
709
+ </button>
655
710
  <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>
656
711
  <input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
657
712
  </div>
@@ -1372,6 +1427,13 @@ export function AgentSheet() {
1372
1427
  </button>
1373
1428
  </div>
1374
1429
  </BottomSheet>
1430
+
1431
+ <SoulLibraryPicker
1432
+ open={soulLibraryOpen}
1433
+ onClose={() => setSoulLibraryOpen(false)}
1434
+ onSelect={(s) => setSoul(s)}
1435
+ />
1436
+ </>
1375
1437
  )
1376
1438
  }
1377
1439
 
@@ -12,6 +12,11 @@ import { CronJobForm } from './cron-job-form'
12
12
 
13
13
  interface Props {
14
14
  agent: Agent
15
+ onEditAgent?: () => void
16
+ onClearHistory?: () => void
17
+ onDeleteAgent?: () => void
18
+ onDeleteChat?: () => void
19
+ isMainChat?: boolean
15
20
  }
16
21
 
17
22
  type InspectorTab = 'overview' | 'files' | 'skills' | 'automations' | 'advanced'
@@ -24,7 +29,7 @@ const TABS: { id: InspectorTab; label: string; openclawOnly?: boolean }[] = [
24
29
  { id: 'advanced', label: 'Advanced' },
25
30
  ]
26
31
 
27
- export function InspectorPanel({ agent }: Props) {
32
+ export function InspectorPanel({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: Props) {
28
33
  const inspectorTab = useAppStore((s) => s.inspectorTab)
29
34
  const setInspectorTab = useAppStore((s) => s.setInspectorTab)
30
35
  const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
@@ -52,7 +57,7 @@ export function InspectorPanel({ agent }: Props) {
52
57
  const agentSchedules = Object.values(schedules).filter((s) => s.agentId === agent.id)
53
58
 
54
59
  return (
55
- <div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden fade-up-delay">
60
+ <div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-bg flex flex-col h-full overflow-hidden fade-up-delay">
56
61
  {/* Header */}
57
62
  <div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
58
63
  <h3 className="font-display text-[14px] font-600 text-text truncate">{agent.name}</h3>
@@ -69,12 +74,14 @@ export function InspectorPanel({ agent }: Props) {
69
74
  </div>
70
75
 
71
76
  {/* Tab bar */}
72
- <div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0">
77
+ <div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0" role="tablist">
73
78
  {visibleTabs.map((tab) => (
74
79
  <button
75
80
  key={tab.id}
81
+ role="tab"
76
82
  onClick={() => setInspectorTab(tab.id)}
77
- className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap
83
+ aria-selected={inspectorTab === tab.id}
84
+ className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap focus-visible:ring-1 focus-visible:ring-accent-bright/50
78
85
  ${inspectorTab === tab.id
79
86
  ? 'bg-accent-soft text-accent-bright'
80
87
  : 'bg-transparent text-text-3 hover:text-text-2'}`}
@@ -88,7 +95,14 @@ export function InspectorPanel({ agent }: Props) {
88
95
  {/* Tab content */}
89
96
  <div className="flex-1 min-h-0 overflow-y-auto">
90
97
  {inspectorTab === 'overview' && (
91
- <OverviewTab agent={agent} />
98
+ <OverviewTab
99
+ agent={agent}
100
+ onEditAgent={onEditAgent}
101
+ onClearHistory={onClearHistory}
102
+ onDeleteAgent={onDeleteAgent}
103
+ onDeleteChat={onDeleteChat}
104
+ isMainChat={isMainChat}
105
+ />
92
106
  )}
93
107
  {inspectorTab === 'files' && isOpenClaw && (
94
108
  <AgentFilesEditor agentId={agent.id} />
@@ -117,7 +131,16 @@ export function InspectorPanel({ agent }: Props) {
117
131
  )
118
132
  }
119
133
 
120
- function OverviewTab({ agent }: { agent: Agent }) {
134
+ interface OverviewTabProps {
135
+ agent: Agent
136
+ onEditAgent?: () => void
137
+ onClearHistory?: () => void
138
+ onDeleteAgent?: () => void
139
+ onDeleteChat?: () => void
140
+ isMainChat?: boolean
141
+ }
142
+
143
+ function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: OverviewTabProps) {
121
144
  return (
122
145
  <div className="p-4 flex flex-col gap-4">
123
146
  <div>
@@ -160,6 +183,58 @@ function OverviewTab({ agent }: { agent: Agent }) {
160
183
  </div>
161
184
  </div>
162
185
  )}
186
+
187
+ {/* Actions */}
188
+ {(onEditAgent || onClearHistory || onDeleteAgent || onDeleteChat) && (
189
+ <>
190
+ <div className="border-t border-white/[0.06] mt-2" />
191
+ <div className="flex flex-col gap-2">
192
+ {onEditAgent && (
193
+ <button
194
+ onClick={onEditAgent}
195
+ className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-accent-bright bg-accent-soft/50 border border-accent-bright/10 cursor-pointer transition-all hover:bg-accent-soft"
196
+ style={{ fontFamily: 'inherit' }}
197
+ >
198
+ Edit Agent
199
+ </button>
200
+ )}
201
+ {(onClearHistory || onDeleteAgent || onDeleteChat) && (
202
+ <>
203
+ <label className="block text-[11px] font-600 uppercase tracking-wider text-red-400/50 mt-2">Danger Zone</label>
204
+ <div className="flex flex-col gap-1.5">
205
+ {onClearHistory && (
206
+ <button
207
+ onClick={onClearHistory}
208
+ className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
209
+ style={{ fontFamily: 'inherit' }}
210
+ >
211
+ Clear History
212
+ </button>
213
+ )}
214
+ {onDeleteAgent && !isMainChat && (
215
+ <button
216
+ onClick={onDeleteAgent}
217
+ className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
218
+ style={{ fontFamily: 'inherit' }}
219
+ >
220
+ Delete Agent
221
+ </button>
222
+ )}
223
+ {onDeleteChat && !isMainChat && (
224
+ <button
225
+ onClick={onDeleteChat}
226
+ className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
227
+ style={{ fontFamily: 'inherit' }}
228
+ >
229
+ Delete Chat
230
+ </button>
231
+ )}
232
+ </div>
233
+ </>
234
+ )}
235
+ </div>
236
+ </>
237
+ )}
163
238
  </div>
164
239
  )
165
240
  }
@@ -23,6 +23,7 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
23
23
  const [saving, setSaving] = useState(false)
24
24
  const [installTarget, setInstallTarget] = useState<OpenClawSkillEntry | null>(null)
25
25
  const [removeTarget, setRemoveTarget] = useState<OpenClawSkillEntry | null>(null)
26
+ const [readinessFilter, setReadinessFilter] = useState<'all' | 'ready' | 'needs-setup'>('all')
26
27
 
27
28
  const loadSkills = useCallback(async () => {
28
29
  setLoading(true)
@@ -67,15 +68,24 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
67
68
  }
68
69
  }
69
70
 
71
+ const readyCount = skills.filter((s) => s.eligible).length
72
+ const needsSetupCount = skills.filter((s) => !s.eligible).length
73
+
74
+ const filteredSkills = readinessFilter === 'all'
75
+ ? skills
76
+ : readinessFilter === 'ready'
77
+ ? skills.filter((s) => s.eligible)
78
+ : skills.filter((s) => !s.eligible)
79
+
70
80
  const grouped = SOURCE_ORDER
71
81
  .map((source) => ({
72
82
  source,
73
- items: skills.filter((s) => s.source === source),
83
+ items: filteredSkills.filter((s) => s.source === source),
74
84
  }))
75
85
  .filter((g) => g.items.length > 0)
76
86
 
77
87
  if (loading) {
78
- return <div className="flex items-center justify-center h-32 text-[13px] text-text-3/50">Loading skills...</div>
88
+ return <div className="flex items-center justify-center gap-2 h-32 text-[13px] text-text-3/50"><span className="w-3 h-3 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin" />Loading skills...</div>
79
89
  }
80
90
 
81
91
  if (error) {
@@ -90,7 +100,7 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
90
100
  <button
91
101
  key={m}
92
102
  onClick={() => handleModeChange(m)}
93
- className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
103
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
94
104
  ${mode === m ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
95
105
  style={{ fontFamily: 'inherit' }}
96
106
  >
@@ -99,6 +109,25 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
99
109
  ))}
100
110
  </div>
101
111
 
112
+ {/* Readiness filter */}
113
+ <div className="flex gap-1">
114
+ {([
115
+ { key: 'all' as const, label: `All (${skills.length})` },
116
+ { key: 'ready' as const, label: `Ready (${readyCount})` },
117
+ { key: 'needs-setup' as const, label: `Needs Setup (${needsSetupCount})` },
118
+ ]).map((f) => (
119
+ <button
120
+ key={f.key}
121
+ onClick={() => setReadinessFilter(f.key)}
122
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
123
+ ${readinessFilter === f.key ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
124
+ style={{ fontFamily: 'inherit' }}
125
+ >
126
+ {f.label}
127
+ </button>
128
+ ))}
129
+ </div>
130
+
102
131
  {/* Skill groups */}
103
132
  {grouped.map(({ source, items }) => (
104
133
  <div key={source}>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
3
+ import { useEffect, useMemo, useState } from 'react'
4
4
  import type { PersonalityDraft } from '@/types'
5
5
  import { api } from '@/lib/api-client'
6
6
  import {
@@ -21,22 +21,33 @@ const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-tex
21
21
 
22
22
  export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
23
23
  const [draft, setDraft] = useState<Record<string, string>>({})
24
+ const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
25
+ const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
24
26
 
25
27
  useEffect(() => {
28
+ let parsed: Record<string, string> = {}
26
29
  if (fileType === 'IDENTITY.md') {
27
- const parsed = parseIdentityMd(content)
28
- setDraft({ name: parsed.name || '', creature: parsed.creature || '', vibe: parsed.vibe || '', emoji: parsed.emoji || '' })
30
+ const p = parseIdentityMd(content)
31
+ parsed = { name: p.name || '', creature: p.creature || '', vibe: p.vibe || '', emoji: p.emoji || '' }
29
32
  } else if (fileType === 'USER.md') {
30
- const parsed = parseUserMd(content)
31
- setDraft({ name: parsed.name || '', callThem: parsed.callThem || '', pronouns: parsed.pronouns || '', timezone: parsed.timezone || '', notes: parsed.notes || '', context: parsed.context || '' })
33
+ const p = parseUserMd(content)
34
+ parsed = { name: p.name || '', callThem: p.callThem || '', pronouns: p.pronouns || '', timezone: p.timezone || '', notes: p.notes || '', context: p.context || '' }
32
35
  } else if (fileType === 'SOUL.md') {
33
- const parsed = parseSoulMd(content)
34
- setDraft({ coreTruths: parsed.coreTruths || '', boundaries: parsed.boundaries || '', vibe: parsed.vibe || '', continuity: parsed.continuity || '' })
36
+ const p = parseSoulMd(content)
37
+ parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
35
38
  }
39
+ setDraft(parsed)
40
+ setInitialDraft(parsed)
41
+ setSaveState('idle')
36
42
  }, [content, fileType])
37
43
 
44
+ const isDirty = useMemo(() => {
45
+ return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
46
+ }, [draft, initialDraft])
47
+
38
48
  const update = (key: string, value: string) => {
39
49
  setDraft((prev) => ({ ...prev, [key]: value }))
50
+ setSaveState('idle')
40
51
  }
41
52
 
42
53
  const handleSave = () => {
@@ -49,6 +60,9 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
49
60
  serialized = serializeSoulMd(draft as PersonalityDraft['soul'])
50
61
  }
51
62
  onSave(serialized)
63
+ setInitialDraft({ ...draft })
64
+ setSaveState('saved')
65
+ setTimeout(() => setSaveState('idle'), 1500)
52
66
  }
53
67
 
54
68
  const fields = fileType === 'IDENTITY.md'
@@ -99,13 +113,27 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
99
113
  )}
100
114
  </div>
101
115
  ))}
102
- <button
103
- onClick={handleSave}
104
- className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110"
105
- style={{ fontFamily: 'inherit' }}
106
- >
107
- Apply to Raw Editor
108
- </button>
116
+ <div className="flex items-center gap-3">
117
+ <button
118
+ onClick={handleSave}
119
+ className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110 focus-visible:ring-1 focus-visible:ring-accent-bright/50"
120
+ style={{ fontFamily: 'inherit' }}
121
+ >
122
+ Apply to Raw Editor
123
+ </button>
124
+ {isDirty && saveState === 'idle' && (
125
+ <span className="inline-flex items-center gap-1.5 text-[11px] text-amber-400 font-600">
126
+ <span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
127
+ Unsaved
128
+ </span>
129
+ )}
130
+ {saveState === 'saved' && (
131
+ <span className="inline-flex items-center gap-1.5 text-[11px] text-emerald-400 font-600">
132
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
133
+ Saved
134
+ </span>
135
+ )}
136
+ </div>
109
137
  </div>
110
138
  )
111
139
  }
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
5
+ import { SOUL_LIBRARY, SOUL_ARCHETYPES, searchSouls, type SoulTemplate } from '@/lib/soul-library'
6
+
7
+ interface SoulLibraryPickerProps {
8
+ open: boolean
9
+ onClose: () => void
10
+ onSelect: (soul: string) => void
11
+ }
12
+
13
+ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPickerProps) {
14
+ const [query, setQuery] = useState('')
15
+ const [archetype, setArchetype] = useState('All')
16
+
17
+ const results = useMemo(() => searchSouls(query, archetype), [query, archetype])
18
+
19
+ const handleSelect = (template: SoulTemplate) => {
20
+ onSelect(template.soul)
21
+ onClose()
22
+ }
23
+
24
+ return (
25
+ <BottomSheet open={open} onClose={onClose}>
26
+ <div className="mb-6">
27
+ <h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
28
+ <p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
29
+ </div>
30
+
31
+ {/* Search */}
32
+ <div className="mb-4">
33
+ <input
34
+ type="text"
35
+ value={query}
36
+ onChange={(e) => setQuery(e.target.value)}
37
+ placeholder="Search personalities..."
38
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none focus-glow"
39
+ style={{ fontFamily: 'inherit' }}
40
+ />
41
+ </div>
42
+
43
+ {/* Archetype filter tabs */}
44
+ <div className="flex gap-1 flex-wrap mb-6">
45
+ {SOUL_ARCHETYPES.map((a) => (
46
+ <button
47
+ key={a}
48
+ onClick={() => setArchetype(a)}
49
+ className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
50
+ ${archetype === a
51
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
52
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
53
+ style={{ fontFamily: 'inherit' }}
54
+ >
55
+ {a}
56
+ </button>
57
+ ))}
58
+ </div>
59
+
60
+ {/* Results grid */}
61
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto pb-4">
62
+ {results.map((template) => (
63
+ <button
64
+ key={template.id}
65
+ onClick={() => handleSelect(template)}
66
+ className="text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-accent-bright/20 transition-all cursor-pointer group"
67
+ style={{ fontFamily: 'inherit' }}
68
+ >
69
+ <div className="flex items-start gap-2 mb-2">
70
+ <h4 className="text-[14px] font-600 text-text group-hover:text-accent-bright transition-colors">
71
+ {template.name}
72
+ </h4>
73
+ <span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] text-text-3 text-[10px] font-600 shrink-0">
74
+ {template.archetype}
75
+ </span>
76
+ </div>
77
+ <p className="text-[12px] text-text-3 mb-2">{template.description}</p>
78
+ <p className="text-[11px] text-text-3/60 line-clamp-2 italic">{template.soul}</p>
79
+ </button>
80
+ ))}
81
+ {results.length === 0 && (
82
+ <p className="text-[13px] text-text-3 col-span-2 text-center py-8">No personalities match your search</p>
83
+ )}
84
+ </div>
85
+
86
+ <p className="text-[11px] text-text-3/50 mt-4 text-center">{SOUL_LIBRARY.length} personalities available</p>
87
+ </BottomSheet>
88
+ )
89
+ }