@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,11 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useRef, useState } from 'react'
4
- import { useChatStore, type PendingFile } from '@/stores/use-chat-store'
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { useChatStore } from '@/stores/use-chat-store'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { uploadImage } from '@/lib/upload'
7
7
  import { useAutoResize } from '@/hooks/use-auto-resize'
8
8
  import { useSpeechRecognition } from '@/hooks/use-speech-recognition'
9
+ import { FilePreview } from '@/components/shared/file-preview'
10
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
9
11
 
10
12
  interface Props {
11
13
  streaming: boolean
@@ -13,36 +15,7 @@ interface Props {
13
15
  onStop: () => void
14
16
  }
15
17
 
16
- function FilePreview({ file, onRemove }: { file: PendingFile; onRemove: () => void }) {
17
- const isImage = file.file.type.startsWith('image/')
18
- return (
19
- <div className="relative">
20
- {isImage ? (
21
- <img
22
- src={URL.createObjectURL(file.file)}
23
- alt="Preview"
24
- className="h-16 rounded-[10px] object-cover border border-white/[0.06]"
25
- />
26
- ) : (
27
- <div className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] border border-white/[0.06] bg-white/[0.03]">
28
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
29
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
30
- <polyline points="14 2 14 8 20 8" />
31
- </svg>
32
- <span className="text-[13px] text-text-2 font-500 truncate max-w-[180px]">{file.file.name}</span>
33
- </div>
34
- )}
35
- <button
36
- onClick={onRemove}
37
- className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full border border-white/10 bg-raised
38
- text-text-2 text-[10px] cursor-pointer flex items-center justify-center
39
- hover:bg-danger-soft hover:text-danger hover:border-danger/20 transition-colors"
40
- >
41
- &times;
42
- </button>
43
- </div>
44
- )
45
- }
18
+ // FilePreview is now imported from @/components/shared/file-preview
46
19
 
47
20
  export function ChatInput({ streaming, onSend, onStop }: Props) {
48
21
  const [value, setValue] = useState('')
@@ -53,16 +26,51 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
53
26
  const addPendingFile = useChatStore((s) => s.addPendingFile)
54
27
  const removePendingFile = useChatStore((s) => s.removePendingFile)
55
28
  const speechRecognitionLang = useAppStore((s) => s.appSettings.speechRecognitionLang)
29
+ const sessionId = useAppStore((s) => s.currentSessionId)
30
+
31
+ const queuedMessages = useChatStore((s) => s.queuedMessages)
32
+ const addQueuedMessage = useChatStore((s) => s.addQueuedMessage)
33
+ const removeQueuedMessage = useChatStore((s) => s.removeQueuedMessage)
34
+
35
+ // Draft persistence: restore on session change
36
+ const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
37
+ useEffect(() => {
38
+ if (!sessionId) return
39
+ const draft = localStorage.getItem(`sc_draft_${sessionId}`)
40
+ setValue(draft || '')
41
+ }, [sessionId])
42
+
43
+ // Debounced save to localStorage
44
+ useEffect(() => {
45
+ if (!sessionId) return
46
+ if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
47
+ draftTimerRef.current = setTimeout(() => {
48
+ if (value) localStorage.setItem(`sc_draft_${sessionId}`, value)
49
+ else localStorage.removeItem(`sc_draft_${sessionId}`)
50
+ }, 300)
51
+ return () => { if (draftTimerRef.current) clearTimeout(draftTimerRef.current) }
52
+ }, [value, sessionId])
56
53
 
57
54
  const handleSend = useCallback(() => {
58
55
  const text = value.trim()
59
- if ((!text && !pendingFiles.length) || streaming) return
56
+ if (!text && !pendingFiles.length) return
57
+ // If streaming, queue the message instead of blocking
58
+ if (streaming) {
59
+ if (text) {
60
+ addQueuedMessage(text)
61
+ setValue('')
62
+ if (textareaRef.current) textareaRef.current.style.height = 'auto'
63
+ }
64
+ return
65
+ }
60
66
  onSend(text || 'See attached file(s).')
61
67
  setValue('')
68
+ if (sessionId) localStorage.removeItem(`sc_draft_${sessionId}`)
62
69
  if (textareaRef.current) {
63
70
  textareaRef.current.style.height = 'auto'
64
71
  }
65
- }, [value, streaming, onSend, pendingFiles.length])
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [value, streaming, onSend, pendingFiles.length, sessionId])
66
74
 
67
75
  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
68
76
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -84,8 +92,8 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
84
92
  try {
85
93
  const result = await uploadImage(file)
86
94
  addPendingFile({ file, path: result.path, url: result.url })
87
- } catch {
88
- // ignore upload errors
95
+ } catch (err: unknown) {
96
+ console.error('File upload failed:', err instanceof Error ? err.message : String(err))
89
97
  }
90
98
  }, [addPendingFile])
91
99
 
@@ -131,6 +139,27 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
131
139
  </div>
132
140
  )}
133
141
 
142
+ {queuedMessages.length > 0 && (
143
+ <div className="flex flex-wrap items-center gap-1.5 mb-2">
144
+ <span className="label-mono text-amber-400/70">Queued</span>
145
+ {queuedMessages.map((msg, i) => (
146
+ <span key={i} className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-amber-500/10 border border-amber-500/15 text-[12px] text-amber-300 font-mono max-w-[200px]">
147
+ <span className="truncate">{msg}</span>
148
+ <button
149
+ type="button"
150
+ onClick={() => removeQueuedMessage(i)}
151
+ className="shrink-0 text-amber-400/60 hover:text-amber-300 border-none bg-transparent cursor-pointer p-0"
152
+ >
153
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
154
+ <line x1="18" y1="6" x2="6" y2="18" />
155
+ <line x1="6" y1="6" x2="18" y2="18" />
156
+ </svg>
157
+ </button>
158
+ </span>
159
+ ))}
160
+ </div>
161
+ )}
162
+
134
163
  <div className="glass rounded-[20px] overflow-hidden
135
164
  shadow-[0_4px_32px_rgba(0,0,0,0.3)] focus-within:border-border-focus focus-within:shadow-[0_4px_32px_rgba(99,102,241,0.08)] transition-all duration-300">
136
165
 
@@ -198,6 +227,31 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
198
227
  </button>
199
228
  )}
200
229
 
230
+ <Tooltip>
231
+ <TooltipTrigger asChild>
232
+ <button
233
+ type="button"
234
+ onClick={() => { useChatStore.getState().clearContext() }}
235
+ disabled={streaming}
236
+ className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
237
+ text-text-3 text-[13px] cursor-pointer hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-200 disabled:opacity-30 disabled:pointer-events-none"
238
+ style={{ fontFamily: 'inherit' }}
239
+ >
240
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
241
+ <line x1="2" y1="12" x2="22" y2="12" />
242
+ <polyline points="8 8 4 12 8 16" />
243
+ <polyline points="16 8 20 12 16 16" />
244
+ </svg>
245
+ <span className="hidden sm:inline">New context</span>
246
+ </button>
247
+ </TooltipTrigger>
248
+ <TooltipContent side="top" sideOffset={8}
249
+ className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[10px] px-3.5 py-2.5 max-w-[220px]">
250
+ <div className="font-display text-[12px] font-600 mb-0.5">New context window</div>
251
+ <div className="text-[11px] text-text-3 leading-[1.4]">Adds a marker — messages above it won&apos;t be sent to the AI. Nothing is deleted.</div>
252
+ </TooltipContent>
253
+ </Tooltip>
254
+
201
255
  <div className="flex-1" />
202
256
 
203
257
  <span className="text-[11px] text-text-3/60 tabular-nums mr-2 font-mono">
@@ -206,17 +260,27 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
206
260
 
207
261
  <button
208
262
  onClick={handleSend}
209
- disabled={!hasContent || streaming}
263
+ disabled={!hasContent}
210
264
  className={`w-9 h-9 rounded-[11px] border-none flex items-center justify-center
211
265
  shrink-0 cursor-pointer transition-all duration-250
212
- ${hasContent && !streaming
213
- ? 'bg-[#6366F1] text-white active:scale-90 shadow-[0_4px_16px_rgba(99,102,241,0.3)]'
266
+ ${hasContent
267
+ ? streaming
268
+ ? 'bg-amber-500/20 text-amber-400 active:scale-90 border border-amber-500/30'
269
+ : 'bg-accent-bright text-white active:scale-90 shadow-[0_4px_16px_rgba(99,102,241,0.3)]'
214
270
  : 'bg-white/[0.04] text-text-3 pointer-events-none'}`}
271
+ title={streaming ? 'Queue message' : 'Send message'}
215
272
  >
216
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
217
- <line x1="12" y1="19" x2="12" y2="5" />
218
- <polyline points="5 12 12 5 19 12" />
219
- </svg>
273
+ {streaming && hasContent ? (
274
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
275
+ <line x1="12" y1="5" x2="12" y2="19" />
276
+ <line x1="5" y1="12" x2="19" y2="12" />
277
+ </svg>
278
+ ) : (
279
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
280
+ <line x1="12" y1="19" x2="12" y2="5" />
281
+ <polyline points="5 12 12 5 19 12" />
282
+ </svg>
283
+ )}
220
284
  </button>
221
285
  </div>
222
286
  </div>
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { Badge } from '@/components/ui/badge'
7
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
7
8
  import type { MemoryEntry } from '@/types'
8
9
 
9
10
  export function KnowledgeList() {
@@ -13,6 +14,8 @@ export function KnowledgeList() {
13
14
  const [error, setError] = useState<string | null>(null)
14
15
  const [activeTag, setActiveTag] = useState<string | null>(null)
15
16
  const searchRef = useRef(search)
17
+ const agents = useAppStore((s) => s.agents)
18
+ const loadAgents = useAppStore((s) => s.loadAgents)
16
19
  const setKnowledgeSheetOpen = useAppStore((s) => s.setKnowledgeSheetOpen)
17
20
  const setEditingKnowledgeId = useAppStore((s) => s.setEditingKnowledgeId)
18
21
 
@@ -40,8 +43,10 @@ export function KnowledgeList() {
40
43
 
41
44
  // Initial load
42
45
  useEffect(() => {
46
+ loadAgents()
43
47
  const timer = setTimeout(() => { void load(searchRef.current, activeTag) }, 0)
44
48
  return () => clearTimeout(timer)
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
50
  }, [load, activeTag])
46
51
 
47
52
  // Debounced search
@@ -120,8 +125,14 @@ export function KnowledgeList() {
120
125
  {entries.length > 0 ? (
121
126
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 px-5 pb-6">
122
127
  {entries.map((entry) => {
123
- const meta = entry.metadata as { tags?: string[] } | undefined
128
+ const meta = entry.metadata as { tags?: string[]; scope?: 'global' | 'agent'; agentIds?: string[] } | undefined
124
129
  const tags = meta?.tags || []
130
+ const entryScope = meta?.scope || 'global'
131
+ const entryAgentIds = meta?.agentIds || []
132
+ const scopeLabel = entryScope === 'global' ? 'Global' : `${entryAgentIds.length} agent(s)`
133
+ const scopedAgents = entryScope === 'agent'
134
+ ? entryAgentIds.map((id) => agents[id]).filter(Boolean)
135
+ : []
125
136
  return (
126
137
  <div
127
138
  key={entry.id}
@@ -162,6 +173,25 @@ export function KnowledgeList() {
162
173
  ))}
163
174
  </div>
164
175
  )}
176
+ <div className="flex items-center gap-2 mt-1.5">
177
+ <span className={`text-[10px] font-600 ${
178
+ entryScope === 'global' ? 'text-emerald-400' : 'text-amber-400'
179
+ }`}>
180
+ {scopeLabel}
181
+ </span>
182
+ {scopedAgents.length > 0 && (
183
+ <div className="flex items-center gap-1.5">
184
+ <div className="flex items-center -space-x-1.5">
185
+ {scopedAgents.slice(0, 5).map((agent) => (
186
+ <AgentAvatar key={agent.id} seed={agent.avatarSeed} name={agent.name} size={16} className="ring-1 ring-surface" />
187
+ ))}
188
+ </div>
189
+ {scopedAgents.length > 5 && (
190
+ <span className="text-[10px] font-600 text-text-3/60 ml-0.5">+{scopedAgents.length - 5}</span>
191
+ )}
192
+ </div>
193
+ )}
194
+ </div>
165
195
  </div>
166
196
  )
167
197
  })}
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
7
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
7
8
  import type { MemoryEntry } from '@/types'
8
9
 
9
10
  const ACCEPTED_TYPES = '.txt,.md,.csv,.json,.jsonl,.html,.xml,.yaml,.yml,.toml,.py,.js,.ts,.tsx,.jsx,.go,.rs,.java,.c,.cpp,.h,.rb,.php,.sh,.sql,.log,.pdf'
@@ -21,16 +22,26 @@ export function KnowledgeSheet() {
21
22
  const open = useAppStore((s) => s.knowledgeSheetOpen)
22
23
  const setOpen = useAppStore((s) => s.setKnowledgeSheetOpen)
23
24
  const editingId = useAppStore((s) => s.editingKnowledgeId)
25
+ const agents = useAppStore((s) => s.agents)
26
+ const loadAgents = useAppStore((s) => s.loadAgents)
24
27
 
25
28
  const [title, setTitle] = useState('')
26
29
  const [content, setContent] = useState('')
27
30
  const [tags, setTags] = useState('')
31
+ const [scope, setScope] = useState<'global' | 'agent'>('global')
32
+ const [agentIds, setAgentIds] = useState<string[]>([])
28
33
  const [saving, setSaving] = useState(false)
29
34
  const [uploadedFile, setUploadedFile] = useState<{ name: string; url: string; size: number } | null>(null)
30
35
  const [uploading, setUploading] = useState(false)
31
36
  const [isDragging, setIsDragging] = useState(false)
32
37
  const dragCounter = useRef(0)
33
38
  const fileInputRef = useRef<HTMLInputElement>(null)
39
+ const agentList = Object.values(agents)
40
+
41
+ useEffect(() => {
42
+ if (open) loadAgents()
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, [open])
34
45
 
35
46
  useEffect(() => {
36
47
  if (!open) return
@@ -38,8 +49,10 @@ export function KnowledgeSheet() {
38
49
  void api<MemoryEntry>('GET', `/knowledge/${editingId}`).then((entry) => {
39
50
  setTitle(entry.title)
40
51
  setContent(entry.content)
41
- const meta = entry.metadata as { tags?: string[] } | undefined
52
+ const meta = entry.metadata as { tags?: string[]; scope?: 'global' | 'agent'; agentIds?: string[] } | undefined
42
53
  setTags(meta?.tags?.join(', ') || '')
54
+ setScope(meta?.scope || 'global')
55
+ setAgentIds(meta?.agentIds || [])
43
56
  }).catch(() => {
44
57
  setOpen(false)
45
58
  })
@@ -47,6 +60,8 @@ export function KnowledgeSheet() {
47
60
  setTitle('')
48
61
  setContent('')
49
62
  setTags('')
63
+ setScope('global')
64
+ setAgentIds([])
50
65
  setUploadedFile(null)
51
66
  }
52
67
  }, [open, editingId, setOpen])
@@ -56,6 +71,8 @@ export function KnowledgeSheet() {
56
71
  setTitle('')
57
72
  setContent('')
58
73
  setTags('')
74
+ setScope('global')
75
+ setAgentIds([])
59
76
  setUploadedFile(null)
60
77
  setIsDragging(false)
61
78
  dragCounter.current = 0
@@ -128,6 +145,16 @@ export function KnowledgeSheet() {
128
145
  if (file) void handleUpload(file)
129
146
  }, [handleUpload])
130
147
 
148
+ const toggleAgent = (id: string) => {
149
+ setAgentIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])
150
+ }
151
+
152
+ const scopeHelperText = scope === 'global'
153
+ ? 'This knowledge will be accessible to all agents'
154
+ : agentIds.length === 0
155
+ ? 'Select which agents can access this knowledge'
156
+ : `${agentIds.length} agent(s) selected`
157
+
131
158
  const handleSave = async () => {
132
159
  setSaving(true)
133
160
  try {
@@ -135,6 +162,8 @@ export function KnowledgeSheet() {
135
162
  title: title.trim() || 'Untitled',
136
163
  content,
137
164
  tags: parseTags(tags),
165
+ scope,
166
+ agentIds: scope === 'agent' ? agentIds : [],
138
167
  }
139
168
 
140
169
  if (editingId) {
@@ -294,6 +323,58 @@ export function KnowledgeSheet() {
294
323
  />
295
324
  </div>
296
325
 
326
+ <div className="mb-8">
327
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Scope</label>
328
+ <div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
329
+ {(['global', 'agent'] as const).map((s) => (
330
+ <button
331
+ key={s}
332
+ onClick={() => setScope(s)}
333
+ className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
334
+ scope === s ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
335
+ }`}
336
+ style={{ fontFamily: 'inherit' }}
337
+ >
338
+ {s === 'global' ? 'Global' : 'Specific'}
339
+ </button>
340
+ ))}
341
+ </div>
342
+ <p className="text-[11px] text-text-3/60 mt-1.5 pl-1">{scopeHelperText}</p>
343
+ </div>
344
+
345
+ {scope === 'agent' && (
346
+ <div className="mb-8">
347
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Agents</label>
348
+ <div className="max-h-[240px] overflow-y-auto rounded-[12px] border border-white/[0.06] bg-white/[0.03]">
349
+ {agentList.length === 0 ? (
350
+ <p className="p-3 text-[12px] text-text-3">No agents available</p>
351
+ ) : (
352
+ agentList.map((agent) => {
353
+ const selected = agentIds.includes(agent.id)
354
+ return (
355
+ <button
356
+ key={agent.id}
357
+ onClick={() => toggleAgent(agent.id)}
358
+ className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition-all cursor-pointer ${
359
+ selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]'
360
+ }`}
361
+ style={{ fontFamily: 'inherit' }}
362
+ >
363
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
364
+ <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
365
+ {selected && (
366
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright shrink-0">
367
+ <polyline points="20 6 9 17 4 12" />
368
+ </svg>
369
+ )}
370
+ </button>
371
+ )
372
+ })
373
+ )}
374
+ </div>
375
+ </div>
376
+ )}
377
+
297
378
  <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
298
379
  <button
299
380
  onClick={onClose}
@@ -305,7 +386,7 @@ export function KnowledgeSheet() {
305
386
  <button
306
387
  onClick={() => { void handleSave() }}
307
388
  disabled={!title.trim() || saving}
308
- className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
389
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
309
390
  style={{ fontFamily: 'inherit' }}
310
391
  >
311
392
  {saving ? 'Saving...' : 'Save'}