@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
@@ -0,0 +1,320 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useCallback, useEffect, useMemo, type KeyboardEvent } from 'react'
4
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
5
+ import { FilePreview } from '@/components/shared/file-preview'
6
+ import { useChatroomStore } from '@/stores/use-chatroom-store'
7
+ import { uploadImage } from '@/lib/upload'
8
+ import type { Agent } from '@/types'
9
+
10
+ interface Props {
11
+ agents: Agent[]
12
+ onSend: (text: string) => void
13
+ disabled?: boolean
14
+ }
15
+
16
+ export function ChatroomInput({ agents, onSend, disabled }: Props) {
17
+ const [text, setText] = useState('')
18
+ const [showMentions, setShowMentions] = useState(false)
19
+ const [mentionFilter, setMentionFilter] = useState('')
20
+ const [selectedIndex, setSelectedIndex] = useState(0)
21
+ const chatroomId = useChatroomStore((s) => s.currentChatroomId)
22
+ const inputRef = useRef<HTMLTextAreaElement>(null)
23
+ const fileInputRef = useRef<HTMLInputElement>(null)
24
+ const imageInputRef = useRef<HTMLInputElement>(null)
25
+
26
+ const pendingFiles = useChatroomStore((s) => s.pendingFiles)
27
+ const addPendingFile = useChatroomStore((s) => s.addPendingFile)
28
+ const removePendingFile = useChatroomStore((s) => s.removePendingFile)
29
+ const replyingTo = useChatroomStore((s) => s.replyingTo)
30
+ const setReplyingTo = useChatroomStore((s) => s.setReplyingTo)
31
+
32
+ // Draft persistence: restore on chatroom change
33
+ const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
34
+ useEffect(() => {
35
+ if (!chatroomId) return
36
+ const draft = localStorage.getItem(`sc_draft_cr_${chatroomId}`)
37
+ setText(draft || '')
38
+ }, [chatroomId])
39
+
40
+ // Debounced save to localStorage
41
+ useEffect(() => {
42
+ if (!chatroomId) return
43
+ if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
44
+ draftTimerRef.current = setTimeout(() => {
45
+ if (text) localStorage.setItem(`sc_draft_cr_${chatroomId}`, text)
46
+ else localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
47
+ }, 300)
48
+ return () => { if (draftTimerRef.current) clearTimeout(draftTimerRef.current) }
49
+ }, [text, chatroomId])
50
+
51
+ const uploadAndAdd = useCallback(async (file: File) => {
52
+ try {
53
+ const result = await uploadImage(file)
54
+ addPendingFile({ file, path: result.path, url: result.url })
55
+ } catch {
56
+ // ignore upload errors
57
+ }
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [])
60
+
61
+ const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
62
+ const items = e.clipboardData?.items
63
+ if (!items) return
64
+ for (const item of items) {
65
+ if (item.type.startsWith('image/')) {
66
+ e.preventDefault()
67
+ const file = item.getAsFile()
68
+ if (file) await uploadAndAdd(file)
69
+ return
70
+ }
71
+ }
72
+ }, [uploadAndAdd])
73
+
74
+ const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
75
+ const files = e.target.files
76
+ if (!files?.length) return
77
+ for (const file of Array.from(files)) {
78
+ await uploadAndAdd(file)
79
+ }
80
+ e.target.value = ''
81
+ }, [uploadAndAdd])
82
+
83
+ const handleChange = useCallback((value: string) => {
84
+ setText(value)
85
+ const cursorPos = inputRef.current?.selectionStart || value.length
86
+ const beforeCursor = value.slice(0, cursorPos)
87
+ const mentionMatch = beforeCursor.match(/@(\S*)$/)
88
+ if (mentionMatch) {
89
+ setShowMentions(true)
90
+ setMentionFilter(mentionMatch[1].toLowerCase())
91
+ setSelectedIndex(0)
92
+ } else {
93
+ setShowMentions(false)
94
+ setMentionFilter('')
95
+ setSelectedIndex(0)
96
+ }
97
+ }, [])
98
+
99
+ const insertMention = useCallback((name: string) => {
100
+ const cursorPos = inputRef.current?.selectionStart || text.length
101
+ const beforeCursor = text.slice(0, cursorPos)
102
+ const afterCursor = text.slice(cursorPos)
103
+ const mentionMatch = beforeCursor.match(/@(\S*)$/)
104
+ if (mentionMatch) {
105
+ const newBefore = beforeCursor.slice(0, mentionMatch.index) + `@${name.replace(/\s+/g, '')} `
106
+ setText(newBefore + afterCursor)
107
+ }
108
+ setShowMentions(false)
109
+ inputRef.current?.focus()
110
+ }, [text])
111
+
112
+ const filteredAgents = agents.filter((a) =>
113
+ a.name.toLowerCase().replace(/\s+/g, '').includes(mentionFilter)
114
+ )
115
+
116
+ // Build highlighted segments for the mirror overlay
117
+ const highlightedSegments = useMemo(() => {
118
+ if (!text) return null
119
+ const parts: React.ReactNode[] = []
120
+ let lastIndex = 0
121
+ const regex = /@\S+/g
122
+ let match: RegExpExecArray | null
123
+ while ((match = regex.exec(text)) !== null) {
124
+ if (match.index > lastIndex) {
125
+ parts.push(text.slice(lastIndex, match.index))
126
+ }
127
+ parts.push(
128
+ <span key={match.index} className="bg-accent-soft/50 text-accent-bright rounded px-0.5">
129
+ {match[0]}
130
+ </span>
131
+ )
132
+ lastIndex = regex.lastIndex
133
+ }
134
+ if (lastIndex < text.length) {
135
+ parts.push(text.slice(lastIndex))
136
+ }
137
+ return parts.length > 0 ? parts : null
138
+ }, [text])
139
+
140
+ const mentionDropdownVisible = showMentions && (filteredAgents.length > 0 || mentionFilter === '')
141
+ const mentionItems = mentionDropdownVisible
142
+ ? ['all', ...filteredAgents.map((a) => a.name)]
143
+ : []
144
+
145
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
146
+ if (mentionDropdownVisible) {
147
+ if (e.key === 'ArrowDown') {
148
+ e.preventDefault()
149
+ setSelectedIndex((i) => (i + 1) % mentionItems.length)
150
+ return
151
+ }
152
+ if (e.key === 'ArrowUp') {
153
+ e.preventDefault()
154
+ setSelectedIndex((i) => (i - 1 + mentionItems.length) % mentionItems.length)
155
+ return
156
+ }
157
+ if (e.key === 'Enter' || e.key === 'Tab') {
158
+ e.preventDefault()
159
+ const selected = mentionItems[selectedIndex]
160
+ if (selected) insertMention(selected)
161
+ return
162
+ }
163
+ }
164
+
165
+ if (e.key === 'Enter' && !e.shiftKey) {
166
+ e.preventDefault()
167
+ if ((text.trim() || pendingFiles.length) && !disabled) {
168
+ onSend(text)
169
+ setText('')
170
+ if (chatroomId) localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
171
+ setShowMentions(false)
172
+ }
173
+ }
174
+ if (e.key === 'Escape') {
175
+ if (replyingTo) {
176
+ setReplyingTo(null)
177
+ }
178
+ setShowMentions(false)
179
+ }
180
+ }
181
+
182
+ return (
183
+ <div className="relative px-4 py-3 border-t border-white/[0.06]">
184
+ {/* Mention dropdown */}
185
+ {mentionDropdownVisible && (
186
+ <div className="absolute bottom-full left-4 right-4 mb-1 bg-raised border border-white/[0.1] rounded-[8px] shadow-xl max-h-[200px] overflow-y-auto z-50">
187
+ <button
188
+ onClick={() => insertMention('all')}
189
+ className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
190
+ selectedIndex === 0 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
191
+ }`}
192
+ >
193
+ <div className="w-5 h-5 rounded-full bg-accent-soft flex items-center justify-center text-[9px] font-700 text-accent-bright">@</div>
194
+ <span className="text-[13px] text-text">all</span>
195
+ <span className="text-[11px] text-text-3 ml-auto">Mention all agents</span>
196
+ </button>
197
+ {filteredAgents.map((agent, i) => (
198
+ <button
199
+ key={agent.id}
200
+ onClick={() => insertMention(agent.name)}
201
+ className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
202
+ selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
203
+ }`}
204
+ >
205
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={20} />
206
+ <span className="text-[13px] text-text">{agent.name}</span>
207
+ </button>
208
+ ))}
209
+ </div>
210
+ )}
211
+
212
+ {/* Reply preview banner */}
213
+ {replyingTo && (
214
+ <div className="flex items-center gap-2 mb-2 px-2 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06]">
215
+ <div className="w-0.5 self-stretch rounded-full bg-accent-bright/50 shrink-0" />
216
+ <div className="flex-1 min-w-0">
217
+ <span className="text-[11px] font-600 text-accent-bright">{replyingTo.senderName}</span>
218
+ <p className="text-[12px] text-text-3 truncate m-0">
219
+ {replyingTo.text.length > 100 ? replyingTo.text.slice(0, 100) + '...' : replyingTo.text}
220
+ </p>
221
+ </div>
222
+ <button
223
+ onClick={() => setReplyingTo(null)}
224
+ className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center hover:bg-white/[0.08] cursor-pointer text-text-3 hover:text-text transition-colors"
225
+ >
226
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
227
+ <line x1="18" y1="6" x2="6" y2="18" />
228
+ <line x1="6" y1="6" x2="18" y2="18" />
229
+ </svg>
230
+ </button>
231
+ </div>
232
+ )}
233
+
234
+ {/* File previews */}
235
+ {pendingFiles.length > 0 && (
236
+ <div className="flex gap-2 mb-2 flex-wrap">
237
+ {pendingFiles.map((f, i) => (
238
+ <FilePreview key={i} file={f} onRemove={() => removePendingFile(i)} />
239
+ ))}
240
+ </div>
241
+ )}
242
+
243
+ <div className="flex items-end gap-2">
244
+ {/* Attach file button */}
245
+ <button
246
+ onClick={() => fileInputRef.current?.click()}
247
+ disabled={disabled}
248
+ className="shrink-0 w-9 h-9 rounded-[8px] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer disabled:opacity-30"
249
+ title="Attach file"
250
+ >
251
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
252
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
253
+ </svg>
254
+ </button>
255
+
256
+ {/* Image button */}
257
+ <button
258
+ onClick={() => imageInputRef.current?.click()}
259
+ disabled={disabled}
260
+ className="shrink-0 w-9 h-9 rounded-[8px] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer disabled:opacity-30"
261
+ title="Attach image"
262
+ >
263
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
264
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
265
+ <circle cx="8.5" cy="8.5" r="1.5" />
266
+ <polyline points="21 15 16 10 5 21" />
267
+ </svg>
268
+ </button>
269
+
270
+ <div className="flex-1 relative rounded-[8px] bg-white/[0.06] border border-white/[0.08] focus-within:border-accent-bright/40">
271
+ {/* Highlight mirror — renders @mentions with accent background behind the transparent textarea */}
272
+ <div
273
+ aria-hidden
274
+ className="absolute inset-0 px-3 py-2 text-[13px] leading-[1.5] break-words whitespace-pre-wrap pointer-events-none overflow-hidden"
275
+ style={{ minHeight: '38px', color: 'transparent' }}
276
+ >
277
+ {highlightedSegments}
278
+ </div>
279
+ <textarea
280
+ ref={inputRef}
281
+ value={text}
282
+ onChange={(e) => handleChange(e.target.value)}
283
+ onKeyDown={handleKeyDown}
284
+ onPaste={handlePaste}
285
+ placeholder="Type a message... Use @ to mention agents"
286
+ disabled={disabled}
287
+ rows={1}
288
+ className="w-full resize-none px-3 py-2 rounded-[8px] bg-transparent text-[13px] text-text placeholder:text-text-3 focus:outline-none max-h-[120px] disabled:opacity-50 relative border-none"
289
+ style={{ minHeight: '38px' }}
290
+ />
291
+ </div>
292
+ <button
293
+ onClick={() => {
294
+ if ((text.trim() || pendingFiles.length) && !disabled) {
295
+ onSend(text)
296
+ setText('')
297
+ if (chatroomId) localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
298
+ setShowMentions(false)
299
+ }
300
+ }}
301
+ disabled={(!text.trim() && !pendingFiles.length) || disabled}
302
+ className="shrink-0 w-9 h-9 rounded-[8px] bg-accent-bright flex items-center justify-center hover:bg-accent-bright/90 transition-all disabled:opacity-30 cursor-pointer"
303
+ >
304
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
305
+ <line x1="22" y1="2" x2="11" y2="13" />
306
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
307
+ </svg>
308
+ </button>
309
+ </div>
310
+
311
+ {/* Hidden file inputs */}
312
+ <input ref={fileInputRef} type="file" multiple
313
+ accept="image/*,.pdf,.txt,.md,.csv,.json,.xml,.html,.js,.ts,.tsx,.jsx,.py,.go,.rs,.java,.c,.cpp,.h,.yml,.yaml,.toml,.env,.log,.sh,.sql,.css,.scss"
314
+ onChange={handleFileChange} className="hidden" />
315
+ <input ref={imageInputRef} type="file" multiple
316
+ accept="image/*"
317
+ onChange={handleFileChange} className="hidden" />
318
+ </div>
319
+ )
320
+ }
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useCallback, useMemo, useState } from 'react'
4
+ import { useChatroomStore } from '@/stores/use-chatroom-store'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { useWs } from '@/hooks/use-ws'
7
+ import type { Chatroom } from '@/types'
8
+ import { EmptyState } from '@/components/shared/empty-state'
9
+
10
+ export function ChatroomList() {
11
+ const chatrooms = useChatroomStore((s) => s.chatrooms)
12
+ const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
13
+ const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
14
+ const setCurrentChatroom = useChatroomStore((s) => s.setCurrentChatroom)
15
+ const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
16
+ const setEditingChatroomId = useChatroomStore((s) => s.setEditingChatroomId)
17
+ const agents = useAppStore((s) => s.agents)
18
+
19
+ const refresh = useCallback(() => {
20
+ loadChatrooms()
21
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ }, [])
23
+
24
+ useEffect(() => { refresh() }, [refresh])
25
+ useWs('chatrooms', refresh, 15_000)
26
+
27
+ const [filter, setFilter] = useState<'all' | 'active' | 'recent'>('all')
28
+
29
+ const sorted = useMemo(() =>
30
+ Object.values(chatrooms).sort(
31
+ (a: Chatroom, b: Chatroom) => b.updatedAt - a.updatedAt
32
+ ), [chatrooms])
33
+
34
+ const filtered = useMemo(() => {
35
+ if (filter === 'all') return sorted
36
+ const now = Date.now()
37
+ return sorted.filter((c) => {
38
+ if (filter === 'active') return now - c.updatedAt < 3_600_000 // 1h
39
+ return now - c.updatedAt < 86_400_000 // 24h
40
+ })
41
+ }, [sorted, filter])
42
+
43
+ return (
44
+ <div className="flex-1 overflow-y-auto">
45
+ {sorted.length === 0 ? (
46
+ <EmptyState
47
+ icon={
48
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
49
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" fill="currentColor" />
50
+ </svg>
51
+ }
52
+ title="No chatrooms yet"
53
+ subtitle="Create one to start a group chat"
54
+ action={{ label: '+ New Chatroom', onClick: () => { setEditingChatroomId(null); setChatroomSheetOpen(true) } }}
55
+ />
56
+ ) : (
57
+ <div className="p-3 space-y-1">
58
+ {sorted.length > 2 && (
59
+ <div className="flex items-center gap-1 px-1 pb-2">
60
+ {(['all', 'active', 'recent'] as const).map((f) => (
61
+ <button
62
+ key={f}
63
+ type="button"
64
+ onClick={() => setFilter(f)}
65
+ data-active={filter === f || undefined}
66
+ className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 border-none cursor-pointer transition-all
67
+ data-[active]:bg-accent-soft data-[active]:text-accent-bright
68
+ bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
69
+ >
70
+ {f}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ )}
75
+ {filtered.map((chatroom) => {
76
+ const isActive = chatroom.id === currentChatroomId
77
+ const memberNames = chatroom.agentIds
78
+ .map((id) => agents[id]?.name)
79
+ .filter(Boolean)
80
+ .slice(0, 3)
81
+ const lastMsg = chatroom.messages[chatroom.messages.length - 1]
82
+
83
+ return (
84
+ <button
85
+ key={chatroom.id}
86
+ onClick={() => setCurrentChatroom(chatroom.id)}
87
+ className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent ${
88
+ isActive
89
+ ? 'bg-accent-soft/60'
90
+ : 'hover:bg-white/[0.04]'
91
+ }`}
92
+ >
93
+ <div className="flex items-center gap-2 mb-0.5">
94
+ <div className="w-7 h-7 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
95
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
96
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
97
+ </svg>
98
+ </div>
99
+ <span className={`text-[13px] font-600 truncate ${isActive ? 'text-accent-bright' : 'text-text'}`}>
100
+ {chatroom.name}
101
+ </span>
102
+ <span className="label-mono ml-auto shrink-0">
103
+ {chatroom.agentIds.length} agents
104
+ </span>
105
+ </div>
106
+ {memberNames.length > 0 && (
107
+ <p className="text-[11px] text-text-3 truncate pl-9">
108
+ {memberNames.join(', ')}{chatroom.agentIds.length > 3 ? ` +${chatroom.agentIds.length - 3}` : ''}
109
+ </p>
110
+ )}
111
+ {lastMsg && (
112
+ <p className="text-[11px] text-text-3/70 truncate pl-9 mt-0.5">
113
+ {lastMsg.senderName}: {lastMsg.text.slice(0, 60)}
114
+ </p>
115
+ )}
116
+ </button>
117
+ )
118
+ })}
119
+ </div>
120
+ )}
121
+ </div>
122
+ )
123
+ }