@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,410 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { genId } from '@/lib/id'
3
+ import { loadChatrooms, saveChatrooms, loadAgents, loadSettings, loadSkills, loadCredentials, decryptKey } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import { notFound } from '@/lib/server/collection-helpers'
6
+ import { streamAgentChat } from '@/lib/server/stream-agent-chat'
7
+ import { getProvider } from '@/lib/providers'
8
+ import type { Chatroom, ChatroomMessage, Agent, Session, Message } from '@/types'
9
+
10
+ export const dynamic = 'force-dynamic'
11
+ export const maxDuration = 300
12
+
13
+ const MAX_CHAIN_DEPTH = 5
14
+
15
+ /** Resolve API key from an agent's credentialId */
16
+ function resolveApiKey(credentialId: string | null | undefined): string | null {
17
+ if (!credentialId) return null
18
+ const creds = loadCredentials()
19
+ const cred = creds[credentialId]
20
+ if (!cred?.encryptedKey) return null
21
+ try { return decryptKey(cred.encryptedKey) } catch { return null }
22
+ }
23
+
24
+ /** Parse @mentions from message text, returns matching agentIds */
25
+ function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
26
+ if (/@all\b/i.test(text)) return [...memberIds]
27
+ const mentionPattern = /@(\S+)/g
28
+ const mentioned: string[] = []
29
+ let match: RegExpExecArray | null
30
+ while ((match = mentionPattern.exec(text)) !== null) {
31
+ const name = match[1].toLowerCase()
32
+ for (const id of memberIds) {
33
+ const agent = agents[id]
34
+ if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
35
+ if (!mentioned.includes(id)) mentioned.push(id)
36
+ }
37
+ }
38
+ }
39
+ return mentioned
40
+ }
41
+
42
+ /** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
43
+ function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
44
+ const selfAgent = agents[agentId]
45
+ const selfName = selfAgent?.name || agentId
46
+
47
+ // Build team profiles with capabilities
48
+ const teamProfiles = chatroom.agentIds
49
+ .filter((id) => id !== agentId)
50
+ .map((id) => {
51
+ const a = agents[id]
52
+ if (!a) return null
53
+ const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
54
+ const desc = a.description || a.soul || 'No description'
55
+ return `- **${a.name}**: ${desc}\n ${tools}`
56
+ })
57
+ .filter(Boolean)
58
+ .join('\n')
59
+
60
+ const recentMessages = chatroom.messages.slice(-30).map((m) => {
61
+ return `[${m.senderName}]: ${m.text}`
62
+ }).join('\n')
63
+
64
+ return [
65
+ `## Chatroom Context`,
66
+ `You are **${selfName}** in chatroom "${chatroom.name}".`,
67
+ selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
68
+ selfAgent?.tools?.length ? `Your tools: ${selfAgent.tools.join(', ')}` : '',
69
+ '',
70
+ '## Team Members',
71
+ teamProfiles || '(no other agents)',
72
+ '',
73
+ '## Collaboration Guidelines',
74
+ '- Before executing complex tasks, briefly discuss your approach with the team.',
75
+ '- When delegating to another agent, explain what you need, why they are best suited, and what output you expect. Example: "@DataBot I need a summary of recent API errors from the logs — you have the shell tool to grep through them."',
76
+ '- If someone mentions a task you are well-suited for, proactively offer to help.',
77
+ '- Do not just @mention mechanically — explain your reasoning when involving others.',
78
+ '- If you can handle a request entirely yourself, just do it. Only delegate what you cannot do.',
79
+ '',
80
+ '## Recent Messages',
81
+ recentMessages || '(no messages yet)',
82
+ ].filter((line) => line !== undefined).join('\n')
83
+ }
84
+
85
+ /** Build a synthetic session object for an agent in a chatroom */
86
+ function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
87
+ return {
88
+ id: `chatroom-${chatroomId}-${agent.id}`,
89
+ name: `Chatroom session for ${agent.name}`,
90
+ cwd: process.cwd(),
91
+ user: 'chatroom',
92
+ provider: agent.provider,
93
+ model: agent.model,
94
+ credentialId: agent.credentialId ?? null,
95
+ fallbackCredentialIds: agent.fallbackCredentialIds,
96
+ apiEndpoint: agent.apiEndpoint ?? null,
97
+ claudeSessionId: null,
98
+ messages: [],
99
+ createdAt: Date.now(),
100
+ lastActiveAt: Date.now(),
101
+ tools: agent.tools || [],
102
+ agentId: agent.id,
103
+ }
104
+ }
105
+
106
+ /** Build agent's system prompt including skills */
107
+ function buildAgentSystemPromptForChatroom(agent: Agent): string {
108
+ const settings = loadSettings()
109
+ const parts: string[] = []
110
+ if (settings.userPrompt) parts.push(settings.userPrompt)
111
+ if (agent.soul) parts.push(agent.soul)
112
+ if (agent.systemPrompt) parts.push(agent.systemPrompt)
113
+ if (agent.skillIds?.length) {
114
+ const allSkills = loadSkills()
115
+ for (const skillId of agent.skillIds) {
116
+ const skill = allSkills[skillId]
117
+ if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
118
+ }
119
+ }
120
+ return parts.join('\n\n')
121
+ }
122
+
123
+ /** Convert chatroom messages to Message history format for LLM */
124
+ function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
125
+ const history = chatroom.messages.slice(-50).map((m) => {
126
+ let msgText = `[${m.senderName}]: ${m.text}`
127
+ // Include attachment info in history
128
+ if (m.attachedFiles?.length) {
129
+ const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
130
+ msgText += `\n[Attached: ${names}]`
131
+ }
132
+ return {
133
+ role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
134
+ text: msgText,
135
+ time: m.time,
136
+ ...(m.imagePath ? { imagePath: m.imagePath } : {}),
137
+ ...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
138
+ }
139
+ })
140
+ // Pass through imagePath/attachedFiles from the current message to the last history entry
141
+ if (history.length > 0 && (imagePath || attachedFiles)) {
142
+ const last = history[history.length - 1]
143
+ if (imagePath && !last.imagePath) last.imagePath = imagePath
144
+ if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
145
+ }
146
+ return history
147
+ }
148
+
149
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
150
+ const { id } = await params
151
+ const body = await req.json()
152
+
153
+ const chatrooms = loadChatrooms()
154
+ const chatroom = chatrooms[id] as Chatroom | undefined
155
+ if (!chatroom) return notFound()
156
+
157
+ const text = typeof body.text === 'string' ? body.text : ''
158
+ const senderId = typeof body.senderId === 'string' ? body.senderId : 'user'
159
+ const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
160
+ const attachedFiles = Array.isArray(body.attachedFiles)
161
+ ? (body.attachedFiles as unknown[]).filter((f): f is string => typeof f === 'string')
162
+ : undefined
163
+ const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
164
+
165
+ if (!text.trim() && !imagePath && !attachedFiles?.length) {
166
+ return NextResponse.json({ error: 'text or attachment is required' }, { status: 400 })
167
+ }
168
+
169
+ const agents = loadAgents() as Record<string, Agent>
170
+
171
+ // Persist incoming message
172
+ const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
173
+ let mentions = parseMentions(text, agents, chatroom.agentIds)
174
+ // Auto-address: if enabled and no explicit mentions, address all agents
175
+ if (chatroom.autoAddress && mentions.length === 0) {
176
+ mentions = [...chatroom.agentIds]
177
+ }
178
+ const userMessage: ChatroomMessage = {
179
+ id: genId(),
180
+ senderId,
181
+ senderName,
182
+ role: senderId === 'user' ? 'user' : 'assistant',
183
+ text,
184
+ mentions,
185
+ reactions: [],
186
+ time: Date.now(),
187
+ ...(imagePath ? { imagePath } : {}),
188
+ ...(attachedFiles ? { attachedFiles } : {}),
189
+ ...(replyToId ? { replyToId } : {}),
190
+ }
191
+ chatroom.messages.push(userMessage)
192
+ chatroom.updatedAt = Date.now()
193
+ chatrooms[id] = chatroom
194
+ saveChatrooms(chatrooms)
195
+ notify('chatrooms')
196
+ notify(`chatroom:${id}`)
197
+
198
+ // Build reply context if replying to a message
199
+ let replyContext = ''
200
+ if (replyToId) {
201
+ const replyMsg = chatroom.messages.find((m) => m.id === replyToId)
202
+ if (replyMsg) {
203
+ const truncated = replyMsg.text.length > 200 ? replyMsg.text.slice(0, 200) + '...' : replyMsg.text
204
+ replyContext = `> [${replyMsg.senderName}]: ${truncated}\n\n`
205
+ }
206
+ }
207
+
208
+ // SSE stream
209
+ const encoder = new TextEncoder()
210
+ const stream = new ReadableStream({
211
+ start(controller) {
212
+ let closed = false
213
+ const writeEvent = (event: Record<string, unknown>) => {
214
+ if (closed) return
215
+ try {
216
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
217
+ } catch {
218
+ closed = true
219
+ }
220
+ }
221
+
222
+ const processAgents = async () => {
223
+ // Build agent queue: start with mentioned agents, then chain
224
+ const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
225
+ const processed = new Set<string>()
226
+ const agentQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = []
227
+
228
+ /** Process a single agent: stream response, persist message, return chained mentions */
229
+ const processOneAgent = async (item: { agentId: string; depth: number; contextMessage?: string }): Promise<string[]> => {
230
+ if (processed.has(item.agentId) || item.depth >= MAX_CHAIN_DEPTH) return []
231
+ processed.add(item.agentId)
232
+
233
+ const agent = agents[item.agentId]
234
+ if (!agent) return []
235
+
236
+ // Pre-flight: check if the agent's provider is usable before attempting to stream
237
+ const providerInfo = getProvider(agent.provider)
238
+ const apiKey = resolveApiKey(agent.credentialId)
239
+ if (providerInfo?.requiresApiKey && !apiKey) {
240
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
241
+ writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
242
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
243
+ return []
244
+ }
245
+ if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
246
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
247
+ writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
248
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
249
+ return []
250
+ }
251
+
252
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
253
+
254
+ try {
255
+ const freshChatrooms = loadChatrooms()
256
+ const freshChatroom = freshChatrooms[id] as Chatroom
257
+
258
+ const syntheticSession = buildSyntheticSession(agent, id)
259
+ const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
260
+ const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
261
+ const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
262
+ const history = buildHistoryForAgent(freshChatroom, agent.id, imagePath, attachedFiles)
263
+
264
+ // Use enriched context message for chained agents, or reply context + original text
265
+ const messageForAgent = item.contextMessage || (replyContext + text)
266
+
267
+ let fullText = ''
268
+ let agentError = ''
269
+ const result = await streamAgentChat({
270
+ session: syntheticSession,
271
+ message: messageForAgent,
272
+ imagePath,
273
+ attachedFiles,
274
+ apiKey,
275
+ systemPrompt: fullSystemPrompt,
276
+ write: (raw: string) => {
277
+ const lines = raw.split('\n').filter(Boolean)
278
+ for (const line of lines) {
279
+ if (!line.startsWith('data: ')) continue
280
+ try {
281
+ const parsed = JSON.parse(line.slice(6).trim())
282
+ if (parsed.t === 'd' && parsed.text) {
283
+ fullText += parsed.text
284
+ writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
285
+ } else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
286
+ writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
287
+ } else if (parsed.t === 'err' && parsed.text) {
288
+ agentError = parsed.text
289
+ writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
290
+ }
291
+ } catch {
292
+ // skip malformed lines
293
+ }
294
+ }
295
+ },
296
+ history,
297
+ })
298
+
299
+ const responseText = result.fullText || fullText
300
+
301
+ // Don't persist empty or error-only messages — they pollute chat history
302
+ if (!responseText.trim() && agentError) {
303
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
304
+ return []
305
+ }
306
+
307
+ if (responseText.trim()) {
308
+ const newMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
309
+ const agentMessage: ChatroomMessage = {
310
+ id: genId(),
311
+ senderId: agent.id,
312
+ senderName: agent.name,
313
+ role: 'assistant',
314
+ text: responseText,
315
+ mentions: newMentions,
316
+ reactions: [],
317
+ time: Date.now(),
318
+ }
319
+ const latestChatrooms = loadChatrooms()
320
+ const latestChatroom = latestChatrooms[id] as Chatroom
321
+ latestChatroom.messages.push(agentMessage)
322
+ latestChatroom.updatedAt = Date.now()
323
+ latestChatrooms[id] = latestChatroom
324
+ saveChatrooms(latestChatrooms)
325
+ notify(`chatroom:${id}`)
326
+
327
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
328
+
329
+ // Return chained agent IDs — enriched context is built below when queuing
330
+ return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
331
+ }
332
+
333
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
334
+ return []
335
+ } catch (err: unknown) {
336
+ const msg = err instanceof Error ? err.message : String(err)
337
+ writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
338
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
339
+ return []
340
+ }
341
+ }
342
+
343
+ if (chatroom.chatMode === 'parallel') {
344
+ // Process initial batch in parallel
345
+ const results = await Promise.all(initialQueue.map(processOneAgent))
346
+ // Chained agents from parallel responses queue sequentially
347
+ for (const chainedIds of results) {
348
+ for (const cid of chainedIds) {
349
+ agentQueue.push({ agentId: cid, depth: 1 })
350
+ }
351
+ }
352
+ } else {
353
+ // Sequential: push initial queue items
354
+ agentQueue.push(...initialQueue)
355
+ }
356
+
357
+ // Process remaining chained agents sequentially with enriched context
358
+ while (agentQueue.length > 0) {
359
+ const item = agentQueue.shift()!
360
+
361
+ // Build enriched context for chained agents by looking at the most recent message
362
+ if (item.depth > 0 && !item.contextMessage) {
363
+ const latestChatrooms = loadChatrooms()
364
+ const latestChatroom = latestChatrooms[id] as Chatroom
365
+ const lastAgentMsg = [...latestChatroom.messages].reverse().find(
366
+ (m) => m.role === 'assistant' && m.senderId !== item.agentId
367
+ )
368
+ if (lastAgentMsg) {
369
+ const truncated = lastAgentMsg.text.length > 500 ? lastAgentMsg.text.slice(0, 500) + '...' : lastAgentMsg.text
370
+ item.contextMessage = `${lastAgentMsg.senderName} said: "${truncated}" — They're requesting your help. Review the conversation and respond.`
371
+ }
372
+ }
373
+
374
+ const chainedIds = await processOneAgent(item)
375
+ for (const cid of chainedIds) {
376
+ agentQueue.push({ agentId: cid, depth: item.depth + 1 })
377
+ }
378
+ }
379
+
380
+ writeEvent({ t: 'done' })
381
+ if (!closed) {
382
+ try { controller.close() } catch { /* already closed */ }
383
+ closed = true
384
+ }
385
+ }
386
+
387
+ processAgents().catch((err) => {
388
+ const msg = err instanceof Error ? err.message : String(err)
389
+ writeEvent({ t: 'err', text: msg })
390
+ writeEvent({ t: 'done' })
391
+ if (!closed) {
392
+ try { controller.close() } catch { /* already closed */ }
393
+ closed = true
394
+ }
395
+ })
396
+ },
397
+ cancel() {
398
+ // Client disconnected
399
+ },
400
+ })
401
+
402
+ return new NextResponse(stream, {
403
+ headers: {
404
+ 'Content-Type': 'text/event-stream',
405
+ 'Cache-Control': 'no-cache',
406
+ 'Connection': 'keep-alive',
407
+ 'X-Accel-Buffering': 'no',
408
+ },
409
+ })
410
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import { genId } from '@/lib/id'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id]
12
+ if (!chatroom) return notFound()
13
+
14
+ const agentId = body.agentId as string
15
+ if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
16
+
17
+ if (!chatroom.agentIds.includes(agentId)) {
18
+ chatroom.agentIds.push(agentId)
19
+
20
+ // Inject a system event message
21
+ const agents = loadAgents()
22
+ const agentName = agents[agentId]?.name || 'Unknown agent'
23
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
24
+ chatroom.messages.push({
25
+ id: genId(),
26
+ senderId: 'system',
27
+ senderName: 'System',
28
+ role: 'assistant',
29
+ text: `${agentName} has joined the chat`,
30
+ mentions: [],
31
+ reactions: [],
32
+ time: Date.now(),
33
+ })
34
+
35
+ chatroom.updatedAt = Date.now()
36
+ chatrooms[id] = chatroom
37
+ saveChatrooms(chatrooms)
38
+ notify('chatrooms')
39
+ notify(`chatroom:${id}`)
40
+ }
41
+
42
+ return NextResponse.json(chatroom)
43
+ }
44
+
45
+ export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
46
+ const { id } = await params
47
+ const body = await req.json()
48
+ const chatrooms = loadChatrooms()
49
+ const chatroom = chatrooms[id]
50
+ if (!chatroom) return notFound()
51
+
52
+ const agentId = body.agentId as string
53
+ if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
54
+
55
+ const wasPresent = chatroom.agentIds.includes(agentId)
56
+ chatroom.agentIds = chatroom.agentIds.filter((aid: string) => aid !== agentId)
57
+
58
+ // Inject a system event message
59
+ if (wasPresent) {
60
+ const agents = loadAgents()
61
+ const agentName = agents[agentId]?.name || 'Unknown agent'
62
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
63
+ chatroom.messages.push({
64
+ id: genId(),
65
+ senderId: 'system',
66
+ senderName: 'System',
67
+ role: 'assistant',
68
+ text: `${agentName} has left the chat`,
69
+ mentions: [],
70
+ reactions: [],
71
+ time: Date.now(),
72
+ })
73
+ }
74
+
75
+ chatroom.updatedAt = Date.now()
76
+ chatrooms[id] = chatroom
77
+ saveChatrooms(chatrooms)
78
+ notify('chatrooms')
79
+ notify(`chatroom:${id}`)
80
+
81
+ return NextResponse.json(chatroom)
82
+ }
@@ -0,0 +1,39 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import type { Chatroom } from '@/types'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id] as Chatroom | undefined
12
+ if (!chatroom) return notFound()
13
+
14
+ const messageId = body.messageId as string
15
+ if (!messageId) {
16
+ return NextResponse.json({ error: 'messageId is required' }, { status: 400 })
17
+ }
18
+
19
+ const message = chatroom.messages.find((m) => m.id === messageId)
20
+ if (!message) {
21
+ return NextResponse.json({ error: 'Message not found' }, { status: 404 })
22
+ }
23
+
24
+ // Toggle: remove if pinned, add if not
25
+ if (!chatroom.pinnedMessageIds) chatroom.pinnedMessageIds = []
26
+ const idx = chatroom.pinnedMessageIds.indexOf(messageId)
27
+ if (idx >= 0) {
28
+ chatroom.pinnedMessageIds.splice(idx, 1)
29
+ } else {
30
+ chatroom.pinnedMessageIds.push(messageId)
31
+ }
32
+
33
+ chatroom.updatedAt = Date.now()
34
+ chatrooms[id] = chatroom
35
+ saveChatrooms(chatrooms)
36
+ notify(`chatroom:${id}`)
37
+
38
+ return NextResponse.json({ ok: true, pinnedMessageIds: chatroom.pinnedMessageIds })
39
+ }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import type { Chatroom, ChatroomMessage, ChatroomReaction } from '@/types'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json()
10
+ const chatrooms = loadChatrooms()
11
+ const chatroom = chatrooms[id] as Chatroom | undefined
12
+ if (!chatroom) return notFound()
13
+
14
+ const messageId = body.messageId as string
15
+ const emoji = body.emoji as string
16
+ const reactorId = (body.reactorId as string) || 'user'
17
+ if (!messageId || !emoji) {
18
+ return NextResponse.json({ error: 'messageId and emoji are required' }, { status: 400 })
19
+ }
20
+
21
+ const message = chatroom.messages.find((m: ChatroomMessage) => m.id === messageId)
22
+ if (!message) {
23
+ return NextResponse.json({ error: 'Message not found' }, { status: 404 })
24
+ }
25
+
26
+ // Toggle: remove if already exists, add if not
27
+ const existingIdx = message.reactions.findIndex(
28
+ (r: ChatroomReaction) => r.emoji === emoji && r.reactorId === reactorId
29
+ )
30
+ if (existingIdx >= 0) {
31
+ message.reactions.splice(existingIdx, 1)
32
+ } else {
33
+ message.reactions.push({ emoji, reactorId, time: Date.now() })
34
+ }
35
+
36
+ chatroom.updatedAt = Date.now()
37
+ chatrooms[id] = chatroom
38
+ saveChatrooms(chatrooms)
39
+ notify(`chatroom:${id}`)
40
+
41
+ return NextResponse.json({ ok: true, reactions: message.reactions })
42
+ }
@@ -0,0 +1,84 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import { genId } from '@/lib/id'
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const chatrooms = loadChatrooms()
10
+ const chatroom = chatrooms[id]
11
+ if (!chatroom) return notFound()
12
+ return NextResponse.json(chatroom)
13
+ }
14
+
15
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
16
+ const { id } = await params
17
+ const body = await req.json()
18
+ const chatrooms = loadChatrooms()
19
+ const chatroom = chatrooms[id]
20
+ if (!chatroom) return notFound()
21
+
22
+ if (body.name !== undefined) chatroom.name = body.name
23
+ if (body.description !== undefined) chatroom.description = body.description
24
+
25
+ // Diff agentIds and inject join/leave system messages
26
+ if (Array.isArray(body.agentIds)) {
27
+ const oldIds = new Set(chatroom.agentIds)
28
+ const newIds = new Set(body.agentIds as string[])
29
+ const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
30
+ const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
31
+
32
+ if (added.length > 0 || removed.length > 0) {
33
+ const agents = loadAgents()
34
+ if (!Array.isArray(chatroom.messages)) chatroom.messages = []
35
+ const now = Date.now()
36
+ let offset = 0
37
+ for (const aid of added) {
38
+ chatroom.messages.push({
39
+ id: genId(),
40
+ senderId: 'system',
41
+ senderName: 'System',
42
+ role: 'assistant',
43
+ text: `${agents[aid]?.name || 'Unknown agent'} has joined the chat`,
44
+ mentions: [],
45
+ reactions: [],
46
+ time: now + offset++,
47
+ })
48
+ }
49
+ for (const aid of removed) {
50
+ chatroom.messages.push({
51
+ id: genId(),
52
+ senderId: 'system',
53
+ senderName: 'System',
54
+ role: 'assistant',
55
+ text: `${agents[aid]?.name || 'Unknown agent'} has left the chat`,
56
+ mentions: [],
57
+ reactions: [],
58
+ time: now + offset++,
59
+ })
60
+ }
61
+ }
62
+
63
+ chatroom.agentIds = body.agentIds
64
+ }
65
+
66
+ chatroom.updatedAt = Date.now()
67
+
68
+ chatrooms[id] = chatroom
69
+ saveChatrooms(chatrooms)
70
+ notify('chatrooms')
71
+ notify(`chatroom:${id}`)
72
+ return NextResponse.json(chatroom)
73
+ }
74
+
75
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
76
+ const { id } = await params
77
+ const chatrooms = loadChatrooms()
78
+ if (!chatrooms[id]) return notFound()
79
+
80
+ delete chatrooms[id]
81
+ saveChatrooms(chatrooms)
82
+ notify('chatrooms')
83
+ return NextResponse.json({ ok: true })
84
+ }