@swarmclawai/swarmclaw 0.5.3 → 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.
- package/README.md +39 -8
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +51 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +24 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +16 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +175 -95
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +14 -5
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +36 -2
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +56 -2
package/src/lib/tasks.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { BoardTask } from '../types'
|
|
|
4
4
|
export const fetchTasks = (includeArchived = false) =>
|
|
5
5
|
api<Record<string, BoardTask>>('GET', `/tasks${includeArchived ? '?includeArchived=true' : ''}`)
|
|
6
6
|
|
|
7
|
-
export const createTask = (data: { title: string; description: string; agentId: string }) =>
|
|
7
|
+
export const createTask = (data: { title: string; description: string; agentId: string; status?: string }) =>
|
|
8
8
|
api<BoardTask>('POST', '/tasks', data)
|
|
9
9
|
|
|
10
10
|
export const updateTask = (id: string, data: Partial<BoardTask>) =>
|
|
@@ -18,3 +18,6 @@ export const archiveTask = (id: string) =>
|
|
|
18
18
|
|
|
19
19
|
export const unarchiveTask = (id: string) =>
|
|
20
20
|
api<BoardTask>('PUT', `/tasks/${id}`, { status: 'backlog' })
|
|
21
|
+
|
|
22
|
+
export const bulkUpdateTasks = (ids: string[], data: { status?: string; agentId?: string | null; projectId?: string | null }) =>
|
|
23
|
+
api<{ updated: number; ids: string[] }>('POST', '/tasks/bulk', { ids, ...data })
|
package/src/lib/view-routes.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AppView } from '@/types'
|
|
2
2
|
|
|
3
|
-
export const DEFAULT_VIEW: AppView = '
|
|
3
|
+
export const DEFAULT_VIEW: AppView = 'home'
|
|
4
4
|
|
|
5
5
|
export const VIEW_TO_PATH: Record<AppView, string> = {
|
|
6
|
+
home: '/',
|
|
6
7
|
agents: '/agents',
|
|
8
|
+
chatrooms: '/chatrooms',
|
|
7
9
|
schedules: '/schedules',
|
|
8
10
|
memory: '/memory',
|
|
9
11
|
tasks: '/tasks',
|
|
@@ -27,3 +29,36 @@ const entries = Object.entries(VIEW_TO_PATH) as [AppView, string][]
|
|
|
27
29
|
export const PATH_TO_VIEW: Record<string, AppView> = Object.fromEntries(
|
|
28
30
|
entries.map(([view, path]) => [path, view]),
|
|
29
31
|
) as Record<string, AppView>
|
|
32
|
+
|
|
33
|
+
/** Views that support deep-linking to a specific entity by ID */
|
|
34
|
+
const VIEWS_WITH_ID = new Set<AppView>(['agents', 'chatrooms'])
|
|
35
|
+
|
|
36
|
+
// Sorted longest-first so "/mcp-servers" matches before "/" etc.
|
|
37
|
+
const sortedPaths = entries
|
|
38
|
+
.map(([view, path]) => ({ view, path }))
|
|
39
|
+
.sort((a, b) => b.path.length - a.path.length)
|
|
40
|
+
|
|
41
|
+
/** Parse a pathname into { view, id }. Returns null for unknown paths. */
|
|
42
|
+
export function parsePath(pathname: string): { view: AppView; id: string | null } | null {
|
|
43
|
+
// Exact match first (no trailing ID)
|
|
44
|
+
const exact = PATH_TO_VIEW[pathname]
|
|
45
|
+
if (exact) return { view: exact, id: null }
|
|
46
|
+
|
|
47
|
+
// Prefix match: "/agents/abc123" → view=agents, id=abc123
|
|
48
|
+
for (const { view, path } of sortedPaths) {
|
|
49
|
+
if (pathname.startsWith(path + '/')) {
|
|
50
|
+
const rest = pathname.slice(path.length + 1)
|
|
51
|
+
if (rest && !rest.includes('/') && VIEWS_WITH_ID.has(view)) {
|
|
52
|
+
return { view, id: decodeURIComponent(rest) }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build a URL path for a view, optionally with an entity ID. */
|
|
60
|
+
export function buildPath(view: AppView, id?: string | null): string {
|
|
61
|
+
const base = VIEW_TO_PATH[view]
|
|
62
|
+
if (id && VIEWS_WITH_ID.has(view)) return `${base}/${encodeURIComponent(id)}`
|
|
63
|
+
return base
|
|
64
|
+
}
|
package/src/lib/ws-client.ts
CHANGED
|
@@ -9,10 +9,20 @@ const listeners = new Map<string, Set<WsCallback>>()
|
|
|
9
9
|
let connected = false
|
|
10
10
|
|
|
11
11
|
function getWsUrl(key: string): string {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const protocol =
|
|
15
|
-
|
|
12
|
+
if (typeof window === 'undefined') return `ws://localhost:3457/ws?key=${encodeURIComponent(key)}`
|
|
13
|
+
|
|
14
|
+
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
|
15
|
+
const pagePort = window.location.port
|
|
16
|
+
const buildPort = process.env.NEXT_PUBLIC_WS_PORT || '3457'
|
|
17
|
+
|
|
18
|
+
// If the page was loaded on a standard HTTP port (80/443/empty) or a port
|
|
19
|
+
// that doesn't match the expected app port, we're likely behind a reverse
|
|
20
|
+
// proxy. Use the page's host directly so the proxy can route /ws traffic.
|
|
21
|
+
const appPort = String((Number(buildPort) || 3457) - 1) // e.g. 3456
|
|
22
|
+
const behindProxy = !pagePort || pagePort === '80' || pagePort === '443' || pagePort !== appPort
|
|
23
|
+
const wsHost = behindProxy ? window.location.host : `${window.location.hostname}:${buildPort}`
|
|
24
|
+
|
|
25
|
+
return `${protocol}://${wsHost}/ws?key=${encodeURIComponent(key)}`
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
function handleMessage(event: MessageEvent) {
|
|
@@ -53,6 +53,7 @@ interface AppState {
|
|
|
53
53
|
|
|
54
54
|
agents: Record<string, Agent>
|
|
55
55
|
loadAgents: () => Promise<void>
|
|
56
|
+
togglePinAgent: (id: string) => void
|
|
56
57
|
|
|
57
58
|
schedules: Record<string, Schedule>
|
|
58
59
|
loadSchedules: () => Promise<void>
|
|
@@ -180,10 +181,18 @@ interface AppState {
|
|
|
180
181
|
fleetFilter: FleetFilter
|
|
181
182
|
setFleetFilter: (filter: FleetFilter) => void
|
|
182
183
|
|
|
184
|
+
// Chat list filter
|
|
185
|
+
chatFilter: 'all' | 'active' | 'recent'
|
|
186
|
+
setChatFilter: (filter: 'all' | 'active' | 'recent') => void
|
|
187
|
+
|
|
183
188
|
// Activity / Audit Trail
|
|
184
189
|
activityEntries: ActivityEntry[]
|
|
185
190
|
loadActivity: (filters?: { entityType?: string; limit?: number }) => Promise<void>
|
|
186
191
|
|
|
192
|
+
// Unread tracking (localStorage-backed)
|
|
193
|
+
lastReadTimestamps: Record<string, number>
|
|
194
|
+
markChatRead: (id: string) => void
|
|
195
|
+
|
|
187
196
|
// Notifications
|
|
188
197
|
notifications: AppNotification[]
|
|
189
198
|
unreadNotificationCount: number
|
|
@@ -200,7 +209,8 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
200
209
|
hydrate: () => {
|
|
201
210
|
if (typeof window === 'undefined') return
|
|
202
211
|
const user = localStorage.getItem('sc_user')
|
|
203
|
-
|
|
212
|
+
const savedAgentId = localStorage.getItem('sc_agent')
|
|
213
|
+
set({ currentUser: user, currentAgentId: savedAgentId, _hydrated: true })
|
|
204
214
|
},
|
|
205
215
|
setUser: (user) => {
|
|
206
216
|
if (user) localStorage.setItem('sc_user', user)
|
|
@@ -305,16 +315,18 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
305
315
|
newSessionOpen: false,
|
|
306
316
|
setNewSessionOpen: (open) => set({ newSessionOpen: open }),
|
|
307
317
|
|
|
308
|
-
activeView: '
|
|
318
|
+
activeView: 'home',
|
|
309
319
|
setActiveView: (view) => set({ activeView: view }),
|
|
310
320
|
|
|
311
321
|
currentAgentId: null,
|
|
312
322
|
setCurrentAgent: async (id) => {
|
|
313
323
|
if (!id) {
|
|
314
324
|
set({ currentAgentId: null })
|
|
325
|
+
if (typeof window !== 'undefined') localStorage.removeItem('sc_agent')
|
|
315
326
|
return
|
|
316
327
|
}
|
|
317
328
|
set({ currentAgentId: id })
|
|
329
|
+
if (typeof window !== 'undefined') localStorage.setItem('sc_agent', id)
|
|
318
330
|
try {
|
|
319
331
|
const user = get().currentUser || 'default'
|
|
320
332
|
const session = await api<Session>('POST', `/agents/${id}/thread`, { user })
|
|
@@ -336,6 +348,14 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
336
348
|
// ignore
|
|
337
349
|
}
|
|
338
350
|
},
|
|
351
|
+
togglePinAgent: (id) => {
|
|
352
|
+
const agents = { ...get().agents }
|
|
353
|
+
if (agents[id]) {
|
|
354
|
+
agents[id] = { ...agents[id], pinned: !agents[id].pinned }
|
|
355
|
+
set({ agents })
|
|
356
|
+
void api('PUT', `/agents/${id}`, { pinned: agents[id].pinned })
|
|
357
|
+
}
|
|
358
|
+
},
|
|
339
359
|
|
|
340
360
|
schedules: {},
|
|
341
361
|
loadSchedules: async () => {
|
|
@@ -584,6 +604,10 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
584
604
|
fleetFilter: 'all',
|
|
585
605
|
setFleetFilter: (filter) => set({ fleetFilter: filter }),
|
|
586
606
|
|
|
607
|
+
// Chat list filter
|
|
608
|
+
chatFilter: 'all' as const,
|
|
609
|
+
setChatFilter: (filter) => set({ chatFilter: filter }),
|
|
610
|
+
|
|
587
611
|
// Activity / Audit Trail
|
|
588
612
|
activityEntries: [],
|
|
589
613
|
loadActivity: async (filters) => {
|
|
@@ -599,6 +623,16 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
599
623
|
}
|
|
600
624
|
},
|
|
601
625
|
|
|
626
|
+
// Unread tracking
|
|
627
|
+
lastReadTimestamps: typeof window !== 'undefined'
|
|
628
|
+
? (() => { try { return JSON.parse(localStorage.getItem('sc_last_read') || '{}') } catch { return {} } })()
|
|
629
|
+
: {},
|
|
630
|
+
markChatRead: (id) => {
|
|
631
|
+
const ts = { ...get().lastReadTimestamps, [id]: Date.now() }
|
|
632
|
+
set({ lastReadTimestamps: ts })
|
|
633
|
+
try { localStorage.setItem('sc_last_read', JSON.stringify(ts)) } catch { /* ignore */ }
|
|
634
|
+
},
|
|
635
|
+
|
|
602
636
|
// Notifications
|
|
603
637
|
notifications: [],
|
|
604
638
|
unreadNotificationCount: 0,
|
|
@@ -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,22 @@ 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
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
// Module-level cadence interval (not in state to avoid re-renders)
|
|
@@ -128,9 +142,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
128
142
|
setSoundEnabled(next)
|
|
129
143
|
set({ soundEnabled: next })
|
|
130
144
|
},
|
|
145
|
+
thinkingText: '',
|
|
146
|
+
thinkingStartTime: 0,
|
|
131
147
|
streamTraces: [],
|
|
132
148
|
voiceConversationActive: false,
|
|
133
149
|
onStreamEvent: null,
|
|
150
|
+
queuedMessages: [],
|
|
151
|
+
addQueuedMessage: (text) => set((s) => ({ queuedMessages: [...s.queuedMessages, text] })),
|
|
152
|
+
removeQueuedMessage: (index) => set((s) => ({ queuedMessages: s.queuedMessages.filter((_, i) => i !== index) })),
|
|
153
|
+
shiftQueuedMessage: () => {
|
|
154
|
+
const q = get().queuedMessages
|
|
155
|
+
if (!q.length) return undefined
|
|
156
|
+
const next = q[0]
|
|
157
|
+
set({ queuedMessages: q.slice(1) })
|
|
158
|
+
return next
|
|
159
|
+
},
|
|
134
160
|
|
|
135
161
|
pendingFiles: [],
|
|
136
162
|
addPendingFile: (f) => set((s) => ({ pendingFiles: [...s.pendingFiles, f] })),
|
|
@@ -141,6 +167,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
141
167
|
get pendingImage() { const files = get().pendingFiles; return files.length ? files[0] : null },
|
|
142
168
|
setPendingImage: (img) => set({ pendingFiles: img ? [img] : [] }),
|
|
143
169
|
|
|
170
|
+
// Reply-to
|
|
171
|
+
replyingTo: null,
|
|
172
|
+
setReplyingTo: (reply) => set({ replyingTo: reply }),
|
|
173
|
+
|
|
144
174
|
previewContent: null,
|
|
145
175
|
setPreviewContent: (content) => set({ previewContent: content }),
|
|
146
176
|
|
|
@@ -150,7 +180,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
150
180
|
setDebugOpen: (open) => set({ debugOpen: open }),
|
|
151
181
|
|
|
152
182
|
sendMessage: async (text: string) => {
|
|
153
|
-
const { pendingFiles } = get()
|
|
183
|
+
const { pendingFiles, replyingTo } = get()
|
|
154
184
|
if ((!text.trim() && !pendingFiles.length) || get().streaming) return
|
|
155
185
|
const sessionId = useAppStore.getState().currentSessionId
|
|
156
186
|
if (!sessionId) return
|
|
@@ -162,6 +192,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
162
192
|
const attachedFiles = pendingFiles.length > 1
|
|
163
193
|
? pendingFiles.map((f) => f.path)
|
|
164
194
|
: undefined
|
|
195
|
+
const replyToId = replyingTo?.message?.replyToId ? undefined : replyingTo?.message ? `msg-${replyingTo.index}` : undefined
|
|
165
196
|
|
|
166
197
|
const userMsg: Message = {
|
|
167
198
|
role: 'user',
|
|
@@ -170,6 +201,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
170
201
|
imagePath,
|
|
171
202
|
imageUrl,
|
|
172
203
|
attachedFiles,
|
|
204
|
+
...(replyToId ? { replyToId } : {}),
|
|
173
205
|
}
|
|
174
206
|
clearCadence()
|
|
175
207
|
set((s) => ({
|
|
@@ -180,8 +212,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
180
212
|
streamToolName: '',
|
|
181
213
|
displayText: '',
|
|
182
214
|
agentStatus: null,
|
|
215
|
+
thinkingText: '',
|
|
216
|
+
thinkingStartTime: Date.now(),
|
|
183
217
|
messages: [...s.messages, userMsg],
|
|
184
218
|
pendingFiles: [],
|
|
219
|
+
replyingTo: null,
|
|
185
220
|
toolEvents: [],
|
|
186
221
|
lastUsage: null,
|
|
187
222
|
}))
|
|
@@ -296,6 +331,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
296
331
|
set({ streamText: fullText })
|
|
297
332
|
if (get().soundEnabled) playError()
|
|
298
333
|
}
|
|
334
|
+
} else if (event.t === 'thinking') {
|
|
335
|
+
set((s) => ({ thinkingText: s.thinkingText + (event.text || '') }))
|
|
299
336
|
} else if (event.t === 'status') {
|
|
300
337
|
try {
|
|
301
338
|
const parsed = JSON.parse(event.text || '{}')
|
|
@@ -306,7 +343,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
306
343
|
} else if (event.t === 'done') {
|
|
307
344
|
// done
|
|
308
345
|
}
|
|
309
|
-
}, attachedFiles)
|
|
346
|
+
}, attachedFiles, { replyToId })
|
|
310
347
|
|
|
311
348
|
clearCadence()
|
|
312
349
|
if (get().soundEnabled && soundFiredStart) playStreamEnd()
|
|
@@ -333,13 +370,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
333
370
|
displayText: '',
|
|
334
371
|
streamPhase: 'thinking' as const,
|
|
335
372
|
streamToolName: '',
|
|
373
|
+
thinkingText: '',
|
|
374
|
+
thinkingStartTime: 0,
|
|
336
375
|
}))
|
|
337
376
|
if (get().ttsEnabled && !get().voiceConversationActive) speak(fullText)
|
|
338
377
|
} else {
|
|
339
|
-
set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '' })
|
|
378
|
+
set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
|
|
340
379
|
}
|
|
341
380
|
|
|
342
381
|
useAppStore.getState().loadSessions()
|
|
382
|
+
|
|
383
|
+
// Auto-dequeue: if there are queued messages, send the next one
|
|
384
|
+
const nextQueued = get().shiftQueuedMessage()
|
|
385
|
+
if (nextQueued) {
|
|
386
|
+
setTimeout(() => get().sendMessage(nextQueued), 100)
|
|
387
|
+
}
|
|
343
388
|
},
|
|
344
389
|
|
|
345
390
|
editAndResend: async (messageIndex: number, newText: string) => {
|
|
@@ -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
|
+
}))
|