@swarmclawai/swarmclaw 0.5.2 → 0.6.0

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 (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -11,6 +11,9 @@ import type { ProviderType, ClaudeSkill } from '@/types'
11
11
  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
12
12
  import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
13
13
  import { AgentAvatar } from './agent-avatar'
14
+ import { AgentPickerList } from '@/components/shared/agent-picker-list'
15
+ import { randomSoul } from '@/lib/soul-suggestions'
16
+ import { SectionLabel } from '@/components/shared/section-label'
14
17
 
15
18
  const HB_PRESETS = [30, 60, 120, 300, 600, 1800, 3600] as const
16
19
 
@@ -92,7 +95,7 @@ export function AgentSheet() {
92
95
  const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
93
96
  const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
94
97
  const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
95
- const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
98
+ // platformAssignScope is derived from isOrchestrator — no separate state needed
96
99
  const [capabilities, setCapabilities] = useState<string[]>([])
97
100
  const [capInput, setCapInput] = useState('')
98
101
  const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
@@ -167,13 +170,13 @@ export function AgentSheet() {
167
170
  setMcpServerIds(editing.mcpServerIds || [])
168
171
  setMcpDisabledTools(editing.mcpDisabledTools || [])
169
172
  setFallbackCredentialIds(editing.fallbackCredentialIds || [])
170
- setPlatformAssignScope(editing.platformAssignScope || 'self')
173
+ // platformAssignScope derived from isOrchestrator — no separate state
171
174
  setCapabilities(editing.capabilities || [])
172
175
  setCapInput('')
173
176
  setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
174
177
  setOpenclawEnabled(editing.provider === 'openclaw')
175
178
  setProjectId(editing.projectId)
176
- setAvatarSeed(editing.avatarSeed || '')
179
+ setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
177
180
  setThinkingLevel(editing.thinkingLevel || '')
178
181
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
179
182
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
@@ -182,7 +185,7 @@ export function AgentSheet() {
182
185
  } else {
183
186
  setName('')
184
187
  setDescription('')
185
- setSoul('')
188
+ setSoul(randomSoul())
186
189
  setSystemPrompt('')
187
190
  setProvider('claude-cli')
188
191
  setModel('')
@@ -195,7 +198,6 @@ export function AgentSheet() {
195
198
  setSkillIds([])
196
199
  setMcpDisabledTools([])
197
200
  setFallbackCredentialIds([])
198
- setPlatformAssignScope('self')
199
201
  setCapabilities([])
200
202
  setCapInput('')
201
203
  setOllamaMode('local')
@@ -292,7 +294,7 @@ export function AgentSheet() {
292
294
  mcpServerIds,
293
295
  mcpDisabledTools: mcpDisabledTools.length ? mcpDisabledTools : undefined,
294
296
  fallbackCredentialIds,
295
- platformAssignScope,
297
+ platformAssignScope: (isOrchestrator ? 'all' : 'self') as 'all' | 'self',
296
298
  capabilities,
297
299
  projectId: projectId || undefined,
298
300
  avatarSeed: avatarSeed.trim() || undefined,
@@ -305,8 +307,10 @@ export function AgentSheet() {
305
307
  }
306
308
  if (editing) {
307
309
  await updateAgent(editing.id, data)
310
+ toast.success('Agent saved')
308
311
  } else {
309
312
  await createAgent(data)
313
+ toast.success('Agent created')
310
314
  }
311
315
  await loadAgents()
312
316
  onClose()
@@ -315,6 +319,7 @@ export function AgentSheet() {
315
319
  const handleDelete = async () => {
316
320
  if (editing) {
317
321
  await deleteAgent(editing.id)
322
+ toast.success('Agent moved to trash')
318
323
  await loadAgents()
319
324
  onClose()
320
325
  }
@@ -452,12 +457,12 @@ export function AgentSheet() {
452
457
  </div>
453
458
 
454
459
  <div className="mb-8">
455
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
460
+ <SectionLabel>Name</SectionLabel>
456
461
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
457
462
  </div>
458
463
 
459
464
  <div className="mb-8">
460
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Avatar</label>
465
+ <SectionLabel>Avatar</SectionLabel>
461
466
  <div className="flex items-center gap-3">
462
467
  <AgentAvatar seed={avatarSeed || null} name={name || 'A'} size={40} />
463
468
  <input
@@ -470,25 +475,29 @@ export function AgentSheet() {
470
475
  />
471
476
  <button
472
477
  type="button"
473
- onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
474
- className="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] shrink-0"
478
+ onClick={() => setAvatarSeed(crypto.randomUUID().slice(0, 8))}
479
+ 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"
475
480
  style={{ fontFamily: 'inherit' }}
481
+ title="Shuffle avatar"
476
482
  >
477
- Randomize
483
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
484
+ <rect x="4" y="4" width="16" height="16" rx="2" />
485
+ <circle cx="9" cy="9" r="1" fill="currentColor" />
486
+ <circle cx="15" cy="15" r="1" fill="currentColor" />
487
+ </svg>
488
+ Shuffle
478
489
  </button>
479
490
  </div>
480
491
  </div>
481
492
 
482
493
  <div className="mb-8">
483
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
494
+ <SectionLabel>Description</SectionLabel>
484
495
  <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
485
496
  </div>
486
497
 
487
498
  {/* Capabilities — hidden for OpenClaw (gateway manages its own capabilities) */}
488
499
  {!openclawEnabled && <div className="mb-8">
489
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
490
- Capabilities <span className="normal-case tracking-normal font-normal text-text-3">(for agent delegation)</span>
491
- </label>
500
+ <SectionLabel>Capabilities <span className="normal-case tracking-normal font-normal text-text-3">(for agent delegation)</span></SectionLabel>
492
501
  <div className="flex flex-wrap gap-1.5 mb-2">
493
502
  {capabilities.map((cap) => (
494
503
  <span
@@ -629,6 +638,20 @@ export function AgentSheet() {
629
638
  </label>
630
639
  <div className="flex items-center gap-2 mb-3">
631
640
  <p className="text-[12px] text-text-3/60">Define the agent&apos;s voice, tone, and personality. Injected before the system prompt.</p>
641
+ <button
642
+ type="button"
643
+ onClick={() => setSoul(randomSoul())}
644
+ 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"
645
+ style={{ fontFamily: 'inherit' }}
646
+ title="Randomize personality"
647
+ >
648
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
649
+ <rect x="4" y="4" width="16" height="16" rx="2" />
650
+ <circle cx="9" cy="9" r="1" fill="currentColor" />
651
+ <circle cx="15" cy="15" r="1" fill="currentColor" />
652
+ </svg>
653
+ Shuffle
654
+ </button>
632
655
  <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>
633
656
  <input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
634
657
  </div>
@@ -854,7 +877,7 @@ export function AgentSheet() {
854
877
  )}
855
878
 
856
879
  {!openclawEnabled && <div className="mb-8">
857
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Provider</label>
880
+ <SectionLabel>Provider</SectionLabel>
858
881
  <div className="grid grid-cols-3 gap-3">
859
882
  {providers.filter((p) => !isOrchestrator || p.id !== 'claude-cli').map((p) => {
860
883
  const isConnected = !p.requiresApiKey || Object.values(credentials).some((c) => c.provider === p.id)
@@ -883,7 +906,7 @@ export function AgentSheet() {
883
906
 
884
907
  {!openclawEnabled && currentProvider && currentProvider.models.length > 0 && (
885
908
  <div className="mb-8">
886
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Model</label>
909
+ <SectionLabel>Model</SectionLabel>
887
910
  <ModelCombobox
888
911
  providerId={currentProvider.id}
889
912
  value={model}
@@ -900,7 +923,7 @@ export function AgentSheet() {
900
923
  {/* Ollama Mode Toggle */}
901
924
  {!openclawEnabled && provider === 'ollama' && (
902
925
  <div className="mb-8">
903
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Mode</label>
926
+ <SectionLabel>Mode</SectionLabel>
904
927
  <div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
905
928
  {(['local', 'cloud'] as const).map((mode) => (
906
929
  <button
@@ -931,9 +954,7 @@ export function AgentSheet() {
931
954
 
932
955
  {!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && (
933
956
  <div className="mb-8">
934
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
935
- API Key{currentProvider?.optionalApiKey && !currentProvider?.requiresApiKey && <span className="normal-case tracking-normal font-normal text-text-3"> (optional)</span>}
936
- </label>
957
+ <SectionLabel>API Key{currentProvider?.optionalApiKey && !currentProvider?.requiresApiKey && <span className="normal-case tracking-normal font-normal text-text-3"> (optional)</span>}</SectionLabel>
937
958
  {providerCredentials.length > 0 && !addingKey ? (
938
959
  <div className="flex gap-2">
939
960
  <select value={credentialId || ''} onChange={(e) => {
@@ -1037,9 +1058,7 @@ export function AgentSheet() {
1037
1058
 
1038
1059
  {currentProvider?.requiresEndpoint && (provider === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
1039
1060
  <div className="mb-8">
1040
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
1041
- {provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}
1042
- </label>
1061
+ <SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}</SectionLabel>
1043
1062
  <input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
1044
1063
  {provider === 'openclaw' && (
1045
1064
  <p className="text-[13px] text-text-3/70 mt-2">The URL of your OpenClaw gateway</p>
@@ -1058,7 +1077,7 @@ export function AgentSheet() {
1058
1077
  <div
1059
1078
  onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
1060
1079
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1061
- ${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1080
+ ${tools.includes(t.id) ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1062
1081
  >
1063
1082
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1064
1083
  ${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
@@ -1082,7 +1101,7 @@ export function AgentSheet() {
1082
1101
  <div
1083
1102
  onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
1084
1103
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1085
- ${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1104
+ ${tools.includes(t.id) ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1086
1105
  >
1087
1106
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1088
1107
  ${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
@@ -1092,22 +1111,6 @@ export function AgentSheet() {
1092
1111
  </label>
1093
1112
  ))}
1094
1113
  </div>
1095
- {(tools.includes('manage_tasks') || tools.includes('manage_schedules')) && (
1096
- <div className="mt-4 ml-1 pt-3 border-t border-white/[0.04]">
1097
- <label className="flex items-center gap-3 cursor-pointer">
1098
- <div
1099
- onClick={() => setPlatformAssignScope((prev) => prev === 'all' ? 'self' : 'all')}
1100
- className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1101
- ${platformAssignScope === 'all' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1102
- >
1103
- <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1104
- ${platformAssignScope === 'all' ? 'left-[22px]' : 'left-0.5'}`} />
1105
- </div>
1106
- <span className="font-display text-[14px] font-600 text-text-2">Assign to Other Agents</span>
1107
- <span className="text-[12px] text-text-3">Allow this agent to assign tasks and schedules to other agents</span>
1108
- </label>
1109
- </div>
1110
- )}
1111
1114
  </div>
1112
1115
  )}
1113
1116
 
@@ -1259,7 +1262,7 @@ export function AgentSheet() {
1259
1262
  enabled ? [...prev, fullName] : prev.filter((x) => x !== fullName)
1260
1263
  )}
1261
1264
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
1262
- ${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1265
+ ${enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1263
1266
  >
1264
1267
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1265
1268
  ${enabled ? 'left-[22px]' : 'left-0.5'}`} />
@@ -1287,35 +1290,25 @@ export function AgentSheet() {
1287
1290
  if (next && provider === 'claude-cli') setProvider('anthropic')
1288
1291
  }}
1289
1292
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
1290
- ${isOrchestrator ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
1293
+ ${isOrchestrator ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1291
1294
  >
1292
1295
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
1293
1296
  ${isOrchestrator ? 'left-[22px]' : 'left-0.5'}`} />
1294
1297
  </div>
1295
- <span className="font-display text-[14px] font-600 text-text-2">Orchestrator</span>
1296
- <span className="text-[12px] text-text-3">Can delegate tasks to other agents</span>
1298
+ <span className="font-display text-[14px] font-600 text-text-2">Can Delegate to Other Agents</span>
1299
+ <span className="text-[12px] text-text-3">Route work to specialized agents and coordinate multi-agent tasks</span>
1297
1300
  </label>
1298
1301
  </div>
1299
1302
  )}
1300
1303
 
1301
1304
  {provider !== 'openclaw' && isOrchestrator && agentOptions.length > 0 && (
1302
1305
  <div className="mb-8">
1303
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Available Agents</label>
1304
- <div className="flex flex-wrap gap-2">
1305
- {agentOptions.map((a) => (
1306
- <button
1307
- key={a.id}
1308
- onClick={() => toggleAgent(a.id)}
1309
- className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
1310
- ${subAgentIds.includes(a.id)
1311
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
1312
- : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
1313
- style={{ fontFamily: 'inherit' }}
1314
- >
1315
- {a.name}
1316
- </button>
1317
- ))}
1318
- </div>
1306
+ <SectionLabel>Available Agents</SectionLabel>
1307
+ <AgentPickerList
1308
+ agents={agentOptions}
1309
+ selected={subAgentIds}
1310
+ onSelect={(id) => toggleAgent(id)}
1311
+ />
1319
1312
  </div>
1320
1313
  )}
1321
1314
 
@@ -1366,7 +1359,7 @@ export function AgentSheet() {
1366
1359
  onClick={handleTestAndSave}
1367
1360
  disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || saving || (!openclawEnabled && testStatus === 'pass')}
1368
1361
  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
1369
- ${testStatus === 'pass' ? 'bg-emerald-600 shadow-[0_4px_20px_rgba(16,185,129,0.25)]' : 'bg-[#6366F1] shadow-[0_4px_20px_rgba(99,102,241,0.25)]'}`}
1362
+ ${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)]'}`}
1370
1363
  style={{ fontFamily: 'inherit' }}
1371
1364
  >
1372
1365
  {openclawEnabled
@@ -88,7 +88,14 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
88
88
  }
89
89
  }
90
90
 
91
- if (checking) return null
91
+ if (checking) return (
92
+ <div className="h-full flex items-center justify-center bg-bg">
93
+ <div
94
+ className="h-6 w-6 rounded-full border-2 border-white/[0.08] border-t-accent-bright"
95
+ style={{ animation: 'spin 0.8s linear infinite' }}
96
+ />
97
+ </div>
98
+ )
92
99
 
93
100
  return (
94
101
  <div className="h-full flex flex-col items-center justify-center px-8 bg-bg relative overflow-hidden">
@@ -192,7 +199,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
192
199
  <button
193
200
  onClick={handleClaimKey}
194
201
  disabled={loading}
195
- className="px-12 py-4 rounded-[16px] border-none bg-[#6366F1] text-white text-[16px] font-display font-600
202
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
196
203
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
197
204
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
198
205
  >
@@ -233,7 +240,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
233
240
  <button
234
241
  type="submit"
235
242
  disabled={!key.trim() || loading}
236
- className="px-12 py-4 rounded-[16px] border-none bg-[#6366F1] text-white text-[16px] font-display font-600
243
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
237
244
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
238
245
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
239
246
  >
@@ -819,7 +819,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
819
819
  <button
820
820
  onClick={saveProviderAndContinue}
821
821
  disabled={(requiresKey && !apiKey.trim()) || saving}
822
- className="px-8 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-display font-600
822
+ className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
823
823
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
824
824
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
825
825
  >
@@ -923,7 +923,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
923
923
  <button
924
924
  onClick={createStarterAgent}
925
925
  disabled={!agentName.trim() || saving}
926
- className="px-10 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-display font-600
926
+ className="px-10 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
927
927
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
928
928
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
929
929
  >
@@ -2,22 +2,25 @@
2
2
 
3
3
  import { useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
5
6
  import { api } from '@/lib/api-client'
6
7
 
7
8
  export function UserPicker() {
8
9
  const setUser = useAppStore((s) => s.setUser)
10
+ const loadSettings = useAppStore((s) => s.loadSettings)
9
11
  const [name, setName] = useState('')
12
+ const [avatarSeed, setAvatarSeed] = useState(() => Math.random().toString(36).slice(2, 10))
10
13
 
11
14
  const handleSubmit = async (e: React.FormEvent) => {
12
15
  e.preventDefault()
13
16
  const trimmed = name.trim()
14
17
  if (!trimmed) return
15
18
  const userName = trimmed.toLowerCase()
16
- // Save server-side so it persists across devices
17
19
  try {
18
- await api('PUT', '/settings', { userName })
20
+ await api('PUT', '/settings', { userName, userAvatarSeed: avatarSeed.trim() || undefined })
19
21
  } catch { /* still set locally */ }
20
22
  setUser(userName)
23
+ loadSettings()
21
24
  }
22
25
 
23
26
  return (
@@ -71,10 +74,35 @@ export function UserPicker() {
71
74
  focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
72
75
  style={{ fontFamily: 'inherit' }}
73
76
  />
77
+
78
+ {/* Avatar picker */}
79
+ <div className="flex flex-col items-center gap-3">
80
+ <AgentAvatar seed={avatarSeed || null} name={name || '?'} size={64} />
81
+ <div className="flex items-center gap-2">
82
+ <input
83
+ type="text"
84
+ value={avatarSeed}
85
+ onChange={(e) => setAvatarSeed(e.target.value)}
86
+ placeholder="Avatar seed"
87
+ className="w-[160px] px-3 py-2 rounded-[10px] border border-white/[0.08] bg-surface
88
+ text-text text-[13px] text-center outline-none transition-all
89
+ focus:border-accent-bright/30"
90
+ />
91
+ <button
92
+ type="button"
93
+ onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
94
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600
95
+ cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
96
+ >
97
+ Randomize
98
+ </button>
99
+ </div>
100
+ </div>
101
+
74
102
  <button
75
103
  type="submit"
76
104
  disabled={!name.trim()}
77
- className="px-12 py-4 rounded-[16px] border-none bg-[#6366F1] text-white text-[16px] font-display font-600
105
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
78
106
  cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
79
107
  shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
80
108
  style={{ fontFamily: 'inherit' }}
@@ -0,0 +1,169 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain' | 'clipboard' | 'delegate' | 'search' | 'message' }> = {
6
+ memory: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
7
+ memory_tool: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
8
+ manage_tasks: { label: 'Created a task', color: '#EC4899', icon: 'clipboard' },
9
+ manage_schedules: { label: 'Scheduled something', color: '#EC4899', icon: 'clipboard' },
10
+ manage_agents: { label: 'Created an agent', color: '#EC4899', icon: 'clipboard' },
11
+ delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
12
+ delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
13
+ delegate_to_opencode_cli: { label: 'Delegated to OpenCode', color: '#38BDF8', icon: 'delegate' },
14
+ web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
15
+ connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
16
+ }
17
+
18
+ function extractSnippet(toolName: string, toolInput: string): string | null {
19
+ try {
20
+ const parsed = JSON.parse(toolInput)
21
+ if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.title) return parsed.title
22
+ if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.key) return parsed.key
23
+ if (toolName === 'manage_tasks' && parsed.title) return parsed.title
24
+ if (toolName === 'manage_schedules' && parsed.name) return parsed.name
25
+ if (toolName === 'manage_agents' && parsed.name) return parsed.name
26
+ if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
27
+ if (toolName === 'web_search' && parsed.query) return parsed.query
28
+ if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
29
+ } catch { /* ignore parse errors */ }
30
+ return null
31
+ }
32
+
33
+ function MomentIcon({ icon, color }: { icon: string; color: string }) {
34
+ switch (icon) {
35
+ case 'brain':
36
+ return (
37
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
38
+ <path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z" />
39
+ <line x1="10" y1="22" x2="14" y2="22" />
40
+ </svg>
41
+ )
42
+ case 'delegate':
43
+ return (
44
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
45
+ <path d="M7 17l9.2-9.2M17 17V7H7" />
46
+ </svg>
47
+ )
48
+ case 'search':
49
+ return (
50
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
51
+ <circle cx="11" cy="11" r="8" />
52
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
53
+ </svg>
54
+ )
55
+ case 'message':
56
+ return (
57
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
58
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
59
+ </svg>
60
+ )
61
+ default: // clipboard
62
+ return (
63
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
64
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
65
+ <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
66
+ </svg>
67
+ )
68
+ }
69
+ }
70
+
71
+ interface Props {
72
+ toolName: string
73
+ toolInput: string
74
+ onDismiss: () => void
75
+ }
76
+
77
+ export function ActivityMoment({ toolName, toolInput, onDismiss }: Props) {
78
+ const config = NOTABLE_TOOLS[toolName]
79
+ const [phase, setPhase] = useState<'in' | 'out'>('in')
80
+
81
+ useEffect(() => {
82
+ const holdTimer = setTimeout(() => setPhase('out'), 2000)
83
+ const dismissTimer = setTimeout(onDismiss, 2500)
84
+ return () => {
85
+ clearTimeout(holdTimer)
86
+ clearTimeout(dismissTimer)
87
+ }
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, [])
90
+
91
+ if (!config) return null
92
+
93
+ const snippet = extractSnippet(toolName, toolInput)
94
+
95
+ return (
96
+ <div
97
+ className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
98
+ style={{
99
+ animation: phase === 'in'
100
+ ? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
101
+ : 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
102
+ }}
103
+ >
104
+ <div
105
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
106
+ style={{
107
+ background: `${config.color}18`,
108
+ border: `1px solid ${config.color}30`,
109
+ }}
110
+ >
111
+ <MomentIcon icon={config.icon} color={config.color} />
112
+ <span className="text-[10px] font-600" style={{ color: config.color }}>
113
+ {config.label}
114
+ </span>
115
+ {snippet && (
116
+ <span className="text-[10px] text-text-3/60 max-w-[120px] truncate">
117
+ {snippet}
118
+ </span>
119
+ )}
120
+ </div>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ export function isNotableTool(name: string): boolean {
126
+ return name in NOTABLE_TOOLS
127
+ }
128
+
129
+ const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
130
+
131
+ export function HeartbeatMoment({ onDismiss }: { onDismiss: () => void }) {
132
+ const [phase, setPhase] = useState<'in' | 'out'>('in')
133
+
134
+ useEffect(() => {
135
+ const holdTimer = setTimeout(() => setPhase('out'), 2000)
136
+ const dismissTimer = setTimeout(onDismiss, 2500)
137
+ return () => {
138
+ clearTimeout(holdTimer)
139
+ clearTimeout(dismissTimer)
140
+ }
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ }, [])
143
+
144
+ return (
145
+ <div
146
+ className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
147
+ style={{
148
+ animation: phase === 'in'
149
+ ? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
150
+ : 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
151
+ }}
152
+ >
153
+ <div
154
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
155
+ style={{
156
+ background: 'rgba(34,197,94,0.1)',
157
+ border: '1px solid rgba(34,197,94,0.2)',
158
+ }}
159
+ >
160
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="#22c55e">
161
+ <path d={HEART_PATH} />
162
+ </svg>
163
+ <span className="text-[10px] font-600" style={{ color: '#22c55e' }}>
164
+ Heartbeat OK
165
+ </span>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
@@ -14,6 +14,7 @@ import {
14
14
  getSessionConnector,
15
15
  } from '@/components/shared/connector-platform-icon'
16
16
  import { AgentAvatar } from '@/components/agents/agent-avatar'
17
+ import { toast } from 'sonner'
17
18
 
18
19
  function shortPath(p: string): string {
19
20
  return (p || '').replace(/^\/Users\/\w+/, '~')
@@ -196,6 +197,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
196
197
  await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: next })
197
198
  await loadSessions()
198
199
  }
200
+ toast.success(`Heartbeat ${next ? 'enabled' : 'disabled'}`)
199
201
  } finally {
200
202
  setHeartbeatSaving(false)
201
203
  }
@@ -80,7 +80,7 @@ export function ChatToolToggles({ session }: Props) {
80
80
  <div
81
81
  onClick={() => toggleTool(tool.id)}
82
82
  className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
83
- ${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.12]'}`}
83
+ ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
84
84
  >
85
85
  <div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all duration-200
86
86
  ${enabled ? 'left-[16px]' : 'left-[2px]'}`} />