@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
@@ -68,6 +68,10 @@ interface ChatState {
68
68
  pendingImage: PendingFile | null
69
69
  setPendingImage: (img: PendingFile | null) => void
70
70
 
71
+ // Reply-to
72
+ replyingTo: { message: Message; index: number } | null
73
+ setReplyingTo: (reply: { message: Message; index: number } | null) => void
74
+
71
75
  devServer: DevServerStatus | null
72
76
  setDevServer: (ds: DevServerStatus | null) => void
73
77
 
@@ -83,12 +87,31 @@ interface ChatState {
83
87
  sendHeartbeat: (sessionId: string) => Promise<void>
84
88
  stopStreaming: () => void
85
89
 
90
+ // Thinking/reasoning text during streaming
91
+ thinkingText: string
92
+ thinkingStartTime: number
93
+
86
94
  // Rich trace blocks during streaming (F13)
87
95
  streamTraces: ChatTraceBlock[]
88
96
 
89
97
  // Voice conversation
90
98
  voiceConversationActive: boolean
91
99
  onStreamEvent: ((event: { t: string; text?: string }) => void) | null
100
+
101
+ // Message queue (send while streaming)
102
+ queuedMessages: string[]
103
+ addQueuedMessage: (text: string) => void
104
+ removeQueuedMessage: (index: number) => void
105
+ shiftQueuedMessage: () => string | undefined
106
+
107
+ // Context clearing
108
+ clearContext: () => Promise<void>
109
+
110
+ // Pagination
111
+ hasMoreMessages: boolean
112
+ loadingMore: boolean
113
+ totalMessages: number
114
+ loadMoreMessages: () => Promise<void>
92
115
  }
93
116
 
94
117
  // Module-level cadence interval (not in state to avoid re-renders)
@@ -116,7 +139,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
116
139
  displayText: '',
117
140
  agentStatus: null,
118
141
  messages: [],
119
- setMessages: (msgs) => set({ messages: msgs, toolEvents: [] }),
142
+ setMessages: (msgs) => set({ messages: msgs, toolEvents: [], hasMoreMessages: false, totalMessages: msgs.length }),
120
143
  toolEvents: [],
121
144
  clearToolEvents: () => set({ toolEvents: [] }),
122
145
  lastUsage: null,
@@ -128,9 +151,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
128
151
  setSoundEnabled(next)
129
152
  set({ soundEnabled: next })
130
153
  },
154
+ thinkingText: '',
155
+ thinkingStartTime: 0,
131
156
  streamTraces: [],
132
157
  voiceConversationActive: false,
133
158
  onStreamEvent: null,
159
+ queuedMessages: [],
160
+ addQueuedMessage: (text) => set((s) => ({ queuedMessages: [...s.queuedMessages, text] })),
161
+ removeQueuedMessage: (index) => set((s) => ({ queuedMessages: s.queuedMessages.filter((_, i) => i !== index) })),
162
+ shiftQueuedMessage: () => {
163
+ const q = get().queuedMessages
164
+ if (!q.length) return undefined
165
+ const next = q[0]
166
+ set({ queuedMessages: q.slice(1) })
167
+ return next
168
+ },
134
169
 
135
170
  pendingFiles: [],
136
171
  addPendingFile: (f) => set((s) => ({ pendingFiles: [...s.pendingFiles, f] })),
@@ -141,6 +176,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
141
176
  get pendingImage() { const files = get().pendingFiles; return files.length ? files[0] : null },
142
177
  setPendingImage: (img) => set({ pendingFiles: img ? [img] : [] }),
143
178
 
179
+ // Reply-to
180
+ replyingTo: null,
181
+ setReplyingTo: (reply) => set({ replyingTo: reply }),
182
+
144
183
  previewContent: null,
145
184
  setPreviewContent: (content) => set({ previewContent: content }),
146
185
 
@@ -150,7 +189,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
150
189
  setDebugOpen: (open) => set({ debugOpen: open }),
151
190
 
152
191
  sendMessage: async (text: string) => {
153
- const { pendingFiles } = get()
192
+ const { pendingFiles, replyingTo } = get()
154
193
  if ((!text.trim() && !pendingFiles.length) || get().streaming) return
155
194
  const sessionId = useAppStore.getState().currentSessionId
156
195
  if (!sessionId) return
@@ -162,6 +201,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
162
201
  const attachedFiles = pendingFiles.length > 1
163
202
  ? pendingFiles.map((f) => f.path)
164
203
  : undefined
204
+ const replyToId = replyingTo?.message?.replyToId ? undefined : replyingTo?.message ? `msg-${replyingTo.index}` : undefined
165
205
 
166
206
  const userMsg: Message = {
167
207
  role: 'user',
@@ -170,6 +210,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
170
210
  imagePath,
171
211
  imageUrl,
172
212
  attachedFiles,
213
+ ...(replyToId ? { replyToId } : {}),
173
214
  }
174
215
  clearCadence()
175
216
  set((s) => ({
@@ -180,8 +221,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
180
221
  streamToolName: '',
181
222
  displayText: '',
182
223
  agentStatus: null,
224
+ thinkingText: '',
225
+ thinkingStartTime: Date.now(),
183
226
  messages: [...s.messages, userMsg],
184
227
  pendingFiles: [],
228
+ replyingTo: null,
185
229
  toolEvents: [],
186
230
  lastUsage: null,
187
231
  }))
@@ -241,7 +285,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
241
285
  set({ displayText: fullText })
242
286
  }
243
287
  } else if (event.t === 'md') {
244
- // Parse metadata events (usage/run/queue). Ignore unknown keys.
288
+ // Parse metadata events (usage/run/queue/thinking). Ignore unknown keys.
245
289
  try {
246
290
  const meta = JSON.parse(event.text || '{}')
247
291
  if (meta.usage) {
@@ -250,6 +294,9 @@ export const useChatStore = create<ChatState>((set, get) => ({
250
294
  if (meta.suggestions) {
251
295
  suggestions = meta.suggestions
252
296
  }
297
+ if (meta.thinking && typeof meta.thinking === 'string') {
298
+ set({ thinkingText: meta.thinking })
299
+ }
253
300
  } catch {
254
301
  // Ignore non-JSON metadata payloads.
255
302
  }
@@ -296,6 +343,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
296
343
  set({ streamText: fullText })
297
344
  if (get().soundEnabled) playError()
298
345
  }
346
+ } else if (event.t === 'thinking') {
347
+ set((s) => ({ thinkingText: s.thinkingText + (event.text || '') }))
299
348
  } else if (event.t === 'status') {
300
349
  try {
301
350
  const parsed = JSON.parse(event.text || '{}')
@@ -306,17 +355,19 @@ export const useChatStore = create<ChatState>((set, get) => ({
306
355
  } else if (event.t === 'done') {
307
356
  // done
308
357
  }
309
- }, attachedFiles)
358
+ }, attachedFiles, { replyToId })
310
359
 
311
360
  clearCadence()
312
361
  if (get().soundEnabled && soundFiredStart) playStreamEnd()
313
362
  if (fullText.trim()) {
314
363
  const currentToolEvents = get().toolEvents
364
+ const thinkingSnapshot = get().thinkingText || undefined
315
365
  const assistantMsg: Message = {
316
366
  role: 'assistant',
317
367
  text: fullText.trim(),
318
368
  time: Date.now(),
319
369
  kind: 'chat',
370
+ thinking: thinkingSnapshot,
320
371
  toolEvents: currentToolEvents.length ? currentToolEvents.map(e => ({
321
372
  name: e.name,
322
373
  input: e.input,
@@ -333,13 +384,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
333
384
  displayText: '',
334
385
  streamPhase: 'thinking' as const,
335
386
  streamToolName: '',
387
+ thinkingText: '',
388
+ thinkingStartTime: 0,
336
389
  }))
337
390
  if (get().ttsEnabled && !get().voiceConversationActive) speak(fullText)
338
391
  } else {
339
- set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '' })
392
+ set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
340
393
  }
341
394
 
342
395
  useAppStore.getState().loadSessions()
396
+
397
+ // Auto-dequeue: if there are queued messages, send the next one
398
+ const nextQueued = get().shiftQueuedMessage()
399
+ if (nextQueued) {
400
+ setTimeout(() => get().sendMessage(nextQueued), 100)
401
+ }
343
402
  },
344
403
 
345
404
  editAndResend: async (messageIndex: number, newText: string) => {
@@ -480,6 +539,55 @@ export const useChatStore = create<ChatState>((set, get) => ({
480
539
  useAppStore.getState().loadSessions()
481
540
  },
482
541
 
542
+ clearContext: async () => {
543
+ const sessionId = useAppStore.getState().currentSessionId
544
+ if (!sessionId || get().streaming) return
545
+ const marker: Message = { role: 'user', text: '', kind: 'context-clear', time: Date.now() }
546
+ set((s) => ({ messages: [...s.messages, marker] }))
547
+ try {
548
+ const key = getStoredAccessKey()
549
+ await fetch(`/api/sessions/${sessionId}/messages`, {
550
+ method: 'POST',
551
+ headers: { 'Content-Type': 'application/json', ...(key ? { 'X-Access-Key': key } : {}) },
552
+ body: JSON.stringify({ kind: 'context-clear' }),
553
+ })
554
+ } catch {
555
+ // Ignore — marker is already in local state
556
+ }
557
+ },
558
+
559
+ hasMoreMessages: false,
560
+ loadingMore: false,
561
+ totalMessages: 0,
562
+ loadMoreMessages: async () => {
563
+ const { messages, loadingMore, hasMoreMessages, totalMessages } = get()
564
+ if (loadingMore || !hasMoreMessages) return
565
+ const sessionId = useAppStore.getState().currentSessionId
566
+ if (!sessionId) return
567
+ set({ loadingMore: true })
568
+ try {
569
+ const key = getStoredAccessKey()
570
+ // Find the earliest message's original index (startIndex tracked on initial load)
571
+ const currentStartIndex = totalMessages - messages.length
572
+ const res = await fetch(`/api/sessions/${sessionId}/messages?limit=100&before=${currentStartIndex}`, {
573
+ headers: key ? { 'X-Access-Key': key } : undefined,
574
+ })
575
+ if (res.ok) {
576
+ const data = await res.json() as { messages: Message[]; total: number; hasMore: boolean; startIndex: number }
577
+ set((s) => ({
578
+ messages: [...data.messages, ...s.messages],
579
+ hasMoreMessages: data.hasMore,
580
+ totalMessages: data.total,
581
+ loadingMore: false,
582
+ }))
583
+ } else {
584
+ set({ loadingMore: false })
585
+ }
586
+ } catch {
587
+ set({ loadingMore: false })
588
+ }
589
+ },
590
+
483
591
  stopStreaming: async () => {
484
592
  const sessionId = useAppStore.getState().currentSessionId
485
593
  if (sessionId) {
@@ -0,0 +1,276 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+ import { api, getStoredAccessKey } from '@/lib/api-client'
5
+ import type { Chatroom, ChatroomMessage, SSEEvent } from '@/types'
6
+ import type { PendingFile } from '@/stores/use-chat-store'
7
+
8
+ interface ToolEvent {
9
+ name: string
10
+ input: string
11
+ output?: string
12
+ }
13
+
14
+ interface StreamingAgent {
15
+ text: string
16
+ name: string
17
+ error?: string
18
+ toolEvents: ToolEvent[]
19
+ }
20
+
21
+ interface ChatroomState {
22
+ chatrooms: Record<string, Chatroom>
23
+ currentChatroomId: string | null
24
+ streaming: boolean
25
+ streamingAgents: Map<string, StreamingAgent>
26
+ chatroomSheetOpen: boolean
27
+ editingChatroomId: string | null
28
+
29
+ // File uploads
30
+ pendingFiles: PendingFile[]
31
+ addPendingFile: (f: PendingFile) => void
32
+ removePendingFile: (index: number) => void
33
+ clearPendingFiles: () => void
34
+
35
+ // Reply-to
36
+ replyingTo: ChatroomMessage | null
37
+ setReplyingTo: (msg: ChatroomMessage | null) => void
38
+
39
+ loadChatrooms: () => Promise<void>
40
+ createChatroom: (data: { name: string; description?: string; agentIds?: string[]; chatMode?: 'sequential' | 'parallel'; autoAddress?: boolean }) => Promise<Chatroom>
41
+ updateChatroom: (id: string, data: Partial<Chatroom>) => Promise<void>
42
+ deleteChatroom: (id: string) => Promise<void>
43
+ setCurrentChatroom: (id: string | null) => void
44
+ sendMessage: (text: string) => Promise<void>
45
+ toggleReaction: (messageId: string, emoji: string) => Promise<void>
46
+ togglePin: (messageId: string) => Promise<void>
47
+ addMember: (agentId: string) => Promise<void>
48
+ removeMember: (agentId: string) => Promise<void>
49
+ setChatroomSheetOpen: (open: boolean) => void
50
+ setEditingChatroomId: (id: string | null) => void
51
+ }
52
+
53
+ export const useChatroomStore = create<ChatroomState>((set, get) => ({
54
+ chatrooms: {},
55
+ currentChatroomId: null,
56
+ streaming: false,
57
+ streamingAgents: new Map(),
58
+ chatroomSheetOpen: false,
59
+ editingChatroomId: null,
60
+
61
+ // File uploads
62
+ pendingFiles: [],
63
+ addPendingFile: (f) => set((s) => ({ pendingFiles: [...s.pendingFiles, f] })),
64
+ removePendingFile: (index) => set((s) => ({ pendingFiles: s.pendingFiles.filter((_, i) => i !== index) })),
65
+ clearPendingFiles: () => set({ pendingFiles: [] }),
66
+
67
+ // Reply-to
68
+ replyingTo: null,
69
+ setReplyingTo: (msg) => set({ replyingTo: msg }),
70
+
71
+ loadChatrooms: async () => {
72
+ const chatrooms = await api<Record<string, Chatroom>>('GET', '/chatrooms')
73
+ set({ chatrooms })
74
+ },
75
+
76
+ createChatroom: async (data) => {
77
+ const chatroom = await api<Chatroom>('POST', '/chatrooms', data)
78
+ set((s) => ({ chatrooms: { ...s.chatrooms, [chatroom.id]: chatroom } }))
79
+ return chatroom
80
+ },
81
+
82
+ updateChatroom: async (id, data) => {
83
+ const chatroom = await api<Chatroom>('PUT', `/chatrooms/${id}`, data)
84
+ set((s) => ({ chatrooms: { ...s.chatrooms, [id]: chatroom } }))
85
+ },
86
+
87
+ deleteChatroom: async (id) => {
88
+ await api('DELETE', `/chatrooms/${id}`)
89
+ set((s) => {
90
+ const chatrooms = { ...s.chatrooms }
91
+ delete chatrooms[id]
92
+ return {
93
+ chatrooms,
94
+ currentChatroomId: s.currentChatroomId === id ? null : s.currentChatroomId,
95
+ }
96
+ })
97
+ },
98
+
99
+ setCurrentChatroom: (id) => set({ currentChatroomId: id }),
100
+
101
+ sendMessage: async (text) => {
102
+ const { currentChatroomId, streaming, pendingFiles, replyingTo } = get()
103
+ if (!currentChatroomId || streaming || (!text.trim() && !pendingFiles.length)) return
104
+
105
+ set({ streaming: true, streamingAgents: new Map(), pendingFiles: [], replyingTo: null })
106
+
107
+ const imagePath = pendingFiles.length > 0 && pendingFiles[0].file.type.startsWith('image/')
108
+ ? pendingFiles[0].path
109
+ : undefined
110
+ const attachedFiles = pendingFiles.length > 0
111
+ ? pendingFiles.map((f) => f.path)
112
+ : undefined
113
+
114
+ const key = getStoredAccessKey()
115
+ try {
116
+ const res = await fetch(`/api/chatrooms/${currentChatroomId}/chat`, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ ...(key ? { 'X-Access-Key': key } : {}),
121
+ },
122
+ body: JSON.stringify({
123
+ text,
124
+ ...(imagePath ? { imagePath } : {}),
125
+ ...(attachedFiles ? { attachedFiles } : {}),
126
+ ...(replyingTo ? { replyToId: replyingTo.id } : {}),
127
+ }),
128
+ })
129
+
130
+ if (!res.ok || !res.body) {
131
+ set({ streaming: false })
132
+ return
133
+ }
134
+
135
+ const reader = res.body.getReader()
136
+ const decoder = new TextDecoder()
137
+ let buf = ''
138
+
139
+ while (true) {
140
+ const { done, value } = await reader.read()
141
+ if (done) break
142
+ buf += decoder.decode(value, { stream: true })
143
+ const lines = buf.split('\n')
144
+ buf = lines.pop() || ''
145
+ for (const line of lines) {
146
+ if (!line.startsWith('data: ')) continue
147
+ try {
148
+ const event = JSON.parse(line.slice(6)) as SSEEvent
149
+ const agentId = event.agentId
150
+ const agentName = event.agentName
151
+
152
+ if (event.t === 'cr_agent_start' && agentId && agentName) {
153
+ set((s) => {
154
+ const agents = new Map(s.streamingAgents)
155
+ agents.set(agentId, { text: '', name: agentName, toolEvents: [] })
156
+ return { streamingAgents: agents }
157
+ })
158
+ } else if (event.t === 'tool_call' && agentId && event.toolName) {
159
+ set((s) => {
160
+ const agents = new Map(s.streamingAgents)
161
+ const existing = agents.get(agentId)
162
+ if (existing) {
163
+ agents.set(agentId, {
164
+ ...existing,
165
+ toolEvents: [...existing.toolEvents, { name: event.toolName!, input: event.toolInput || '' }],
166
+ })
167
+ }
168
+ return { streamingAgents: agents }
169
+ })
170
+ } else if (event.t === 'tool_result' && agentId) {
171
+ set((s) => {
172
+ const agents = new Map(s.streamingAgents)
173
+ const existing = agents.get(agentId)
174
+ if (existing && existing.toolEvents.length > 0) {
175
+ const updatedEvents = [...existing.toolEvents]
176
+ const last = updatedEvents[updatedEvents.length - 1]
177
+ updatedEvents[updatedEvents.length - 1] = { ...last, output: event.toolOutput || event.text || '' }
178
+ agents.set(agentId, { ...existing, toolEvents: updatedEvents })
179
+ }
180
+ return { streamingAgents: agents }
181
+ })
182
+ } else if (event.t === 'd' && agentId && event.text) {
183
+ set((s) => {
184
+ const agents = new Map(s.streamingAgents)
185
+ const existing = agents.get(agentId)
186
+ if (existing) {
187
+ agents.set(agentId, { ...existing, text: existing.text + event.text })
188
+ }
189
+ return { streamingAgents: agents }
190
+ })
191
+ } else if (event.t === 'err' && agentId && event.text) {
192
+ set((s) => {
193
+ const agents = new Map(s.streamingAgents)
194
+ const existing = agents.get(agentId)
195
+ if (existing) {
196
+ agents.set(agentId, { ...existing, error: event.text })
197
+ }
198
+ return { streamingAgents: agents }
199
+ })
200
+ } else if (event.t === 'cr_agent_done' && agentId) {
201
+ const currentAgent = get().streamingAgents.get(agentId)
202
+ if (currentAgent?.error) {
203
+ setTimeout(() => {
204
+ set((s) => {
205
+ const agents = new Map(s.streamingAgents)
206
+ agents.delete(agentId)
207
+ return { streamingAgents: agents }
208
+ })
209
+ }, 4000)
210
+ } else {
211
+ set((s) => {
212
+ const agents = new Map(s.streamingAgents)
213
+ agents.delete(agentId)
214
+ return { streamingAgents: agents }
215
+ })
216
+ }
217
+ try {
218
+ const { currentChatroomId: cid } = get()
219
+ if (cid) {
220
+ const chatroom = await api<Chatroom>('GET', `/chatrooms/${cid}`)
221
+ set((s) => ({ chatrooms: { ...s.chatrooms, [cid]: chatroom } }))
222
+ }
223
+ } catch { /* will catch on next WS push */ }
224
+ } else if (event.t === 'done') {
225
+ break
226
+ }
227
+ } catch {
228
+ // skip malformed
229
+ }
230
+ }
231
+ }
232
+ } finally {
233
+ set({ streaming: false, streamingAgents: new Map() })
234
+ try {
235
+ const { currentChatroomId: cid } = get()
236
+ if (cid) {
237
+ const chatroom = await api<Chatroom>('GET', `/chatrooms/${cid}`)
238
+ set((s) => ({ chatrooms: { ...s.chatrooms, [cid]: chatroom } }))
239
+ }
240
+ } catch { /* ignore */ }
241
+ }
242
+ },
243
+
244
+ toggleReaction: async (messageId, emoji) => {
245
+ const { currentChatroomId } = get()
246
+ if (!currentChatroomId) return
247
+ await api('POST', `/chatrooms/${currentChatroomId}/reactions`, { messageId, emoji })
248
+ const chatroom = await api<Chatroom>('GET', `/chatrooms/${currentChatroomId}`)
249
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
250
+ },
251
+
252
+ togglePin: async (messageId) => {
253
+ const { currentChatroomId } = get()
254
+ if (!currentChatroomId) return
255
+ await api('POST', `/chatrooms/${currentChatroomId}/pins`, { messageId })
256
+ const chatroom = await api<Chatroom>('GET', `/chatrooms/${currentChatroomId}`)
257
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
258
+ },
259
+
260
+ addMember: async (agentId) => {
261
+ const { currentChatroomId } = get()
262
+ if (!currentChatroomId) return
263
+ const chatroom = await api<Chatroom>('POST', `/chatrooms/${currentChatroomId}/members`, { agentId })
264
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
265
+ },
266
+
267
+ removeMember: async (agentId) => {
268
+ const { currentChatroomId } = get()
269
+ if (!currentChatroomId) return
270
+ const chatroom = await api<Chatroom>('DELETE', `/chatrooms/${currentChatroomId}/members`, { agentId })
271
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
272
+ },
273
+
274
+ setChatroomSheetOpen: (open) => set({ chatroomSheetOpen: open }),
275
+ setEditingChatroomId: (id) => set({ editingChatroomId: id }),
276
+ }))