@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -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 +53 -1
  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/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -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
+ }
@@ -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,96 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+ import { useWs } from '@/hooks/use-ws'
5
+ import { api } from '@/lib/api-client'
6
+
7
+ interface CanvasPanelProps {
8
+ sessionId: string
9
+ agentName?: string
10
+ onClose: () => void
11
+ }
12
+
13
+ export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
14
+ const [content, setContent] = useState<string | null>(null)
15
+
16
+ const loadCanvas = useCallback(async () => {
17
+ try {
18
+ const res = await api<{ content: string | null }>('GET', `/canvas/${sessionId}`)
19
+ setContent(res.content)
20
+ } catch { /* ignore */ }
21
+ }, [sessionId])
22
+
23
+ useEffect(() => { loadCanvas() }, [loadCanvas]) // eslint-disable-line react-hooks/set-state-in-effect
24
+ useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
25
+
26
+ if (!content) return (
27
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
28
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
29
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
30
+ <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
31
+ </svg>
32
+ <span className="text-[13px] font-600 text-text flex-1 truncate">
33
+ Canvas{agentName ? ` — ${agentName}` : ''}
34
+ </span>
35
+ <button
36
+ onClick={onClose}
37
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
38
+ title="Close canvas"
39
+ aria-label="Close canvas"
40
+ >
41
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
42
+ <path d="M18 6L6 18M6 6l12 12" />
43
+ </svg>
44
+ </button>
45
+ </div>
46
+ <div className="flex-1 flex items-center justify-center">
47
+ <div className="text-center">
48
+ <div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
49
+ <span className="text-[13px] text-text-3">Loading canvas...</span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ )
54
+
55
+ return (
56
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
57
+ {/* Toolbar */}
58
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
59
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
60
+ <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
61
+ </svg>
62
+ <span className="text-[13px] font-600 text-text flex-1 truncate">
63
+ Canvas{agentName ? ` — ${agentName}` : ''}
64
+ </span>
65
+ <button
66
+ onClick={loadCanvas}
67
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
68
+ title="Refresh"
69
+ >
70
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
71
+ <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
72
+ </svg>
73
+ </button>
74
+ <button
75
+ onClick={onClose}
76
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
77
+ title="Close canvas"
78
+ >
79
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
80
+ <path d="M18 6L6 18M6 6l12 12" />
81
+ </svg>
82
+ </button>
83
+ </div>
84
+
85
+ {/* Sandboxed iframe */}
86
+ <div className="flex-1 overflow-hidden">
87
+ <iframe
88
+ sandbox="allow-scripts allow-same-origin"
89
+ srcDoc={content}
90
+ className="w-full h-full border-none bg-white"
91
+ title="Agent Canvas"
92
+ />
93
+ </div>
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,173 @@
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
+ delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
15
+ check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
16
+ web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
17
+ connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
18
+ }
19
+
20
+ function extractSnippet(toolName: string, toolInput: string): string | null {
21
+ try {
22
+ const parsed = JSON.parse(toolInput)
23
+ if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.title) return parsed.title
24
+ if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.key) return parsed.key
25
+ if (toolName === 'manage_tasks' && parsed.title) return parsed.title
26
+ if (toolName === 'manage_schedules' && parsed.name) return parsed.name
27
+ if (toolName === 'manage_agents' && parsed.name) return parsed.name
28
+ if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
29
+ if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
30
+ if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
31
+ if (toolName === 'web_search' && parsed.query) return parsed.query
32
+ if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
33
+ } catch { /* ignore parse errors */ }
34
+ return null
35
+ }
36
+
37
+ function MomentIcon({ icon, color }: { icon: string; color: string }) {
38
+ switch (icon) {
39
+ case 'brain':
40
+ return (
41
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
42
+ <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" />
43
+ <line x1="10" y1="22" x2="14" y2="22" />
44
+ </svg>
45
+ )
46
+ case 'delegate':
47
+ return (
48
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
49
+ <path d="M7 17l9.2-9.2M17 17V7H7" />
50
+ </svg>
51
+ )
52
+ case 'search':
53
+ return (
54
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
55
+ <circle cx="11" cy="11" r="8" />
56
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
57
+ </svg>
58
+ )
59
+ case 'message':
60
+ return (
61
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
62
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
63
+ </svg>
64
+ )
65
+ default: // clipboard
66
+ return (
67
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
68
+ <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" />
69
+ <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
70
+ </svg>
71
+ )
72
+ }
73
+ }
74
+
75
+ interface Props {
76
+ toolName: string
77
+ toolInput: string
78
+ onDismiss: () => void
79
+ }
80
+
81
+ export function ActivityMoment({ toolName, toolInput, onDismiss }: Props) {
82
+ const config = NOTABLE_TOOLS[toolName]
83
+ const [phase, setPhase] = useState<'in' | 'out'>('in')
84
+
85
+ useEffect(() => {
86
+ const holdTimer = setTimeout(() => setPhase('out'), 2000)
87
+ const dismissTimer = setTimeout(onDismiss, 2500)
88
+ return () => {
89
+ clearTimeout(holdTimer)
90
+ clearTimeout(dismissTimer)
91
+ }
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [])
94
+
95
+ if (!config) return null
96
+
97
+ const snippet = extractSnippet(toolName, toolInput)
98
+
99
+ return (
100
+ <div
101
+ className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
102
+ style={{
103
+ animation: phase === 'in'
104
+ ? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
105
+ : 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
106
+ }}
107
+ >
108
+ <div
109
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
110
+ style={{
111
+ background: 'var(--card)',
112
+ border: `1px solid ${config.color}40`,
113
+ }}
114
+ >
115
+ <MomentIcon icon={config.icon} color={config.color} />
116
+ <span className="text-[10px] font-600" style={{ color: config.color }}>
117
+ {config.label}
118
+ </span>
119
+ {snippet && (
120
+ <span className="text-[10px] text-text-3/60 max-w-[120px] truncate">
121
+ {snippet}
122
+ </span>
123
+ )}
124
+ </div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ export function isNotableTool(name: string): boolean {
130
+ return name in NOTABLE_TOOLS
131
+ }
132
+
133
+ 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'
134
+
135
+ export function HeartbeatMoment({ onDismiss }: { onDismiss: () => void }) {
136
+ const [phase, setPhase] = useState<'in' | 'out'>('in')
137
+
138
+ useEffect(() => {
139
+ const holdTimer = setTimeout(() => setPhase('out'), 2000)
140
+ const dismissTimer = setTimeout(onDismiss, 2500)
141
+ return () => {
142
+ clearTimeout(holdTimer)
143
+ clearTimeout(dismissTimer)
144
+ }
145
+ // eslint-disable-next-line react-hooks/exhaustive-deps
146
+ }, [])
147
+
148
+ return (
149
+ <div
150
+ className="absolute bottom-full left-0 z-10 pointer-events-none mb-1.5"
151
+ style={{
152
+ animation: phase === 'in'
153
+ ? 'activity-moment-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards'
154
+ : 'activity-moment-out 0.4s cubic-bezier(0.4, 0, 1, 1) forwards',
155
+ }}
156
+ >
157
+ <div
158
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
159
+ style={{
160
+ background: 'var(--card)',
161
+ border: '1px solid rgba(34,197,94,0.3)',
162
+ }}
163
+ >
164
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="#22c55e">
165
+ <path d={HEART_PATH} />
166
+ </svg>
167
+ <span className="text-[10px] font-600" style={{ color: '#22c55e' }}>
168
+ Heartbeat OK
169
+ </span>
170
+ </div>
171
+ </div>
172
+ )
173
+ }