@swarmclawai/swarmclaw 0.4.0 → 0.5.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 +21 -4
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +13 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +39 -22
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +17 -7
- package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +2 -2
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +27 -0
- package/src/app/api/openclaw/discover/route.ts +62 -0
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +31 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +20 -2
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +14 -4
- package/src/app/api/sessions/route.ts +8 -4
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +60 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +84 -3
- package/src/components/agents/agent-sheet.tsx +147 -14
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +41 -6
- package/src/components/chat/chat-header.tsx +305 -29
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +98 -19
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +123 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/connector-platform-icon.tsx +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +74 -0
- package/src/components/skills/skill-list.tsx +2 -1
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-list.tsx +5 -2
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +181 -0
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/lib/id.ts +6 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +28 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +82 -6
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
- package/src/lib/server/connectors/bluebubbles.ts +360 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +51 -8
- package/src/lib/server/connectors/manager.ts +424 -13
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +65 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +8 -9
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +497 -0
- package/src/lib/server/orchestrator-lg.ts +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +24 -11
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +2 -2
- package/src/lib/server/session-tools/connector.ts +53 -6
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +22 -6
- package/src/lib/server/session-tools/file.ts +192 -19
- package/src/lib/server/session-tools/index.ts +4 -2
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +33 -0
- package/src/lib/server/session-tools/search-providers.ts +277 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +53 -72
- package/src/lib/server/storage.ts +74 -11
- package/src/lib/server/stream-agent-chat.ts +53 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +80 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +162 -6
- package/src/types/index.ts +154 -3
- package/tsconfig.json +13 -4
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
4
4
|
import type { Message } from '@/types'
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
7
8
|
import { MessageBubble } from './message-bubble'
|
|
8
9
|
import { StreamingBubble } from './streaming-bubble'
|
|
9
10
|
import { ThinkingIndicator } from './thinking-indicator'
|
|
11
|
+
import { SuggestionsBar } from './suggestions-bar'
|
|
12
|
+
import { ExecApprovalCard } from './exec-approval-card'
|
|
13
|
+
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
10
14
|
|
|
11
15
|
function dateSeparator(ts: number): string {
|
|
12
16
|
const d = new Date(ts)
|
|
@@ -26,14 +30,19 @@ interface Props {
|
|
|
26
30
|
export function MessageList({ messages, streaming }: Props) {
|
|
27
31
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
28
32
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
29
|
-
const
|
|
33
|
+
const snapUntilRef = useRef(0)
|
|
30
34
|
const prevSessionIdRef = useRef<string | null>(null)
|
|
31
|
-
const
|
|
35
|
+
const displayText = useChatStore((s) => s.displayText)
|
|
36
|
+
const setMessages = useChatStore((s) => s.setMessages)
|
|
32
37
|
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
38
|
+
const editAndResend = useChatStore((s) => s.editAndResend)
|
|
39
|
+
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
40
|
+
const forkSession = useAppStore((s) => s.forkSession)
|
|
33
41
|
const session = useAppStore((s) => {
|
|
34
42
|
const id = s.currentSessionId
|
|
35
43
|
return id ? s.sessions[id] : null
|
|
36
44
|
})
|
|
45
|
+
const sessionId = session?.id ?? null
|
|
37
46
|
const agents = useAppStore((s) => s.agents)
|
|
38
47
|
const agent = session?.agentId ? agents[session.agentId] : null
|
|
39
48
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
@@ -49,6 +58,37 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
49
58
|
const [unreadCount, setUnreadCount] = useState(0)
|
|
50
59
|
const prevMsgCountRef = useRef(messages.length)
|
|
51
60
|
|
|
61
|
+
// Bookmark filter
|
|
62
|
+
const [bookmarkFilter, setBookmarkFilter] = useState(false)
|
|
63
|
+
|
|
64
|
+
const toggleBookmark = useCallback(async (index: number) => {
|
|
65
|
+
if (!sessionId) return
|
|
66
|
+
const msg = messages[index]
|
|
67
|
+
if (!msg) return
|
|
68
|
+
const next = !msg.bookmarked
|
|
69
|
+
try {
|
|
70
|
+
await api('PUT', `/sessions/${sessionId}/messages`, { messageIndex: index, bookmarked: next })
|
|
71
|
+
const updated = [...messages]
|
|
72
|
+
updated[index] = { ...updated[index], bookmarked: next }
|
|
73
|
+
setMessages(updated)
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
console.error('Failed to toggle bookmark:', err instanceof Error ? err.message : String(err))
|
|
76
|
+
}
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [sessionId, messages])
|
|
79
|
+
|
|
80
|
+
const handleEditResend = useCallback(async (index: number, newText: string) => {
|
|
81
|
+
if (!sessionId || !editAndResend) return
|
|
82
|
+
await editAndResend(index, newText)
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
}, [sessionId])
|
|
85
|
+
|
|
86
|
+
const handleFork = useCallback(async (index: number) => {
|
|
87
|
+
if (!sessionId || !forkSession) return
|
|
88
|
+
await forkSession(sessionId, index)
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, [sessionId])
|
|
91
|
+
|
|
52
92
|
// In-thread search
|
|
53
93
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
54
94
|
const [searchQuery, setSearchQuery] = useState('')
|
|
@@ -56,9 +96,9 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
56
96
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
57
97
|
|
|
58
98
|
const isHeartbeatMessage = (msg: Message) =>
|
|
59
|
-
msg.role === 'assistant' && (msg.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(msg.text || ''))
|
|
99
|
+
msg.role === 'assistant' && (msg.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(msg.text || '') || /^\s*NO_MESSAGE\b/i.test(msg.text || ''))
|
|
60
100
|
const isHeartbeatOk = (msg: Message) =>
|
|
61
|
-
msg.suppressed === true || (msg.kind === 'heartbeat' && /^\s*HEARTBEAT_OK\b/i.test(msg.text || ''))
|
|
101
|
+
msg.suppressed === true || (msg.kind === 'heartbeat' && (/^\s*HEARTBEAT_OK\b/i.test(msg.text || '') || /^\s*NO_MESSAGE\b/i.test(msg.text || '')))
|
|
62
102
|
|
|
63
103
|
const displayedMessages: Message[] = []
|
|
64
104
|
for (const msg of messages) {
|
|
@@ -79,9 +119,14 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
121
|
|
|
122
|
+
// Apply bookmark filter
|
|
123
|
+
const filteredMessages = bookmarkFilter
|
|
124
|
+
? displayedMessages.filter((msg) => msg.bookmarked)
|
|
125
|
+
: displayedMessages
|
|
126
|
+
|
|
82
127
|
// Search matches
|
|
83
128
|
const searchMatches = searchQuery.trim()
|
|
84
|
-
?
|
|
129
|
+
? filteredMessages
|
|
85
130
|
.map((msg, i) => ({ msg, i }))
|
|
86
131
|
.filter(({ msg }) => msg.text.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
87
132
|
: []
|
|
@@ -99,6 +144,10 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
99
144
|
const nearBottom = isNearBottom(el)
|
|
100
145
|
wasAtBottomRef.current = nearBottom
|
|
101
146
|
setShowScrollToBottom(!nearBottom)
|
|
147
|
+
// Cancel snap window if user manually scrolls away
|
|
148
|
+
if (!nearBottom && Date.now() < snapUntilRef.current) {
|
|
149
|
+
snapUntilRef.current = 0
|
|
150
|
+
}
|
|
102
151
|
if (nearBottom && unreadRef.current > 0) {
|
|
103
152
|
unreadRef.current = 0
|
|
104
153
|
setUnreadCount(0)
|
|
@@ -115,31 +164,52 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
115
164
|
}
|
|
116
165
|
}, [messages.length, isNearBottom])
|
|
117
166
|
|
|
118
|
-
// Detect session switch
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
prevSessionIdRef.current
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
// Detect session switch — set snap window and reset scroll state.
|
|
168
|
+
// Must fire before the scroll positioning layoutEffect below.
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
if (sessionId !== prevSessionIdRef.current) {
|
|
171
|
+
prevSessionIdRef.current = sessionId
|
|
172
|
+
wasAtBottomRef.current = true
|
|
173
|
+
snapUntilRef.current = Date.now() + 2000
|
|
174
|
+
}
|
|
175
|
+
}, [sessionId])
|
|
125
176
|
|
|
126
|
-
|
|
177
|
+
// Position scroll before paint — no setState here to avoid cascading renders.
|
|
178
|
+
// The onScroll handler and the state-update effect below handle UI state.
|
|
179
|
+
useLayoutEffect(() => {
|
|
127
180
|
const el = scrollRef.current
|
|
128
|
-
if (!el) return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
if (!el || messages.length === 0) return
|
|
182
|
+
|
|
183
|
+
const snapping = Date.now() < snapUntilRef.current
|
|
184
|
+
|
|
185
|
+
if (snapping || wasAtBottomRef.current) {
|
|
132
186
|
el.scrollTop = el.scrollHeight
|
|
133
|
-
setShowScrollToBottom(false)
|
|
134
187
|
wasAtBottomRef.current = true
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
// Auto-scroll if user was at bottom before new content arrived
|
|
138
|
-
if (wasAtBottomRef.current) {
|
|
139
|
-
el.scrollTop = el.scrollHeight
|
|
140
188
|
}
|
|
189
|
+
}, [messages.length, displayText])
|
|
190
|
+
|
|
191
|
+
// Update scroll-related UI state after render (separate from layoutEffect to avoid cascading)
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const el = scrollRef.current
|
|
194
|
+
if (!el || messages.length === 0) return
|
|
141
195
|
updateScrollState()
|
|
142
|
-
}, [messages.length,
|
|
196
|
+
}, [messages.length, displayText, updateScrollState])
|
|
197
|
+
|
|
198
|
+
// Re-snap when content resizes during snap window (lazy images increasing scrollHeight)
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
const el = scrollRef.current
|
|
201
|
+
if (!el) return
|
|
202
|
+
const content = el.firstElementChild as HTMLElement | null
|
|
203
|
+
if (!content) return
|
|
204
|
+
|
|
205
|
+
const observer = new ResizeObserver(() => {
|
|
206
|
+
if (Date.now() < snapUntilRef.current || wasAtBottomRef.current) {
|
|
207
|
+
el.scrollTop = el.scrollHeight
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
observer.observe(content)
|
|
211
|
+
return () => observer.disconnect()
|
|
212
|
+
}, [sessionId])
|
|
143
213
|
|
|
144
214
|
const handleScrollToBottom = useCallback(() => {
|
|
145
215
|
const el = scrollRef.current
|
|
@@ -179,7 +249,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
179
249
|
}, [searchOpen])
|
|
180
250
|
|
|
181
251
|
return (
|
|
182
|
-
<div className="relative flex-1 min-h-0">
|
|
252
|
+
<div className="relative flex-1 min-h-0 min-w-0">
|
|
183
253
|
{/* In-thread search bar */}
|
|
184
254
|
{searchOpen && (
|
|
185
255
|
<div className="absolute top-0 left-0 right-0 z-20 flex items-center gap-2 px-6 md:px-12 lg:px-16 py-2 bg-surface/95 backdrop-blur-sm border-b border-white/[0.06]">
|
|
@@ -224,6 +294,15 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
224
294
|
>
|
|
225
295
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 9 6 6 6-6" /></svg>
|
|
226
296
|
</button>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => setBookmarkFilter((v) => !v)}
|
|
299
|
+
aria-label={bookmarkFilter ? 'Show all messages' : 'Show bookmarked only'}
|
|
300
|
+
className={`p-1 rounded-[6px] hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors ${bookmarkFilter ? 'text-[#F59E0B]' : 'text-text-3 hover:text-text-2'}`}
|
|
301
|
+
>
|
|
302
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill={bookmarkFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
303
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
304
|
+
</svg>
|
|
305
|
+
</button>
|
|
227
306
|
<button
|
|
228
307
|
onClick={() => { setSearchOpen(false); setSearchQuery(''); setSearchIdx(0) }}
|
|
229
308
|
aria-label="Close search"
|
|
@@ -240,14 +319,16 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
240
319
|
className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6"
|
|
241
320
|
>
|
|
242
321
|
<div className="flex flex-col gap-6">
|
|
243
|
-
{
|
|
322
|
+
{filteredMessages.map((msg, i) => {
|
|
323
|
+
// Find original index in the full messages array for API calls
|
|
324
|
+
const originalIndex = messages.indexOf(msg)
|
|
244
325
|
const isLastAssistant = msg.role === 'assistant' && !streaming
|
|
245
|
-
&&
|
|
326
|
+
&& filteredMessages.slice(i + 1).every((m) => m.role !== 'assistant')
|
|
246
327
|
const isSearchMatch = searchQuery && searchMatches.some((m) => m.i === i)
|
|
247
328
|
const isCurrentMatch = searchQuery && searchMatches[searchIdx]?.i === i
|
|
248
329
|
|
|
249
330
|
// Date separator
|
|
250
|
-
const prevMsg = i > 0 ?
|
|
331
|
+
const prevMsg = i > 0 ? filteredMessages[i - 1] : null
|
|
251
332
|
const showDateSep = msg.time && (!prevMsg?.time || new Date(msg.time).toDateString() !== new Date(prevMsg.time).toDateString())
|
|
252
333
|
|
|
253
334
|
return (
|
|
@@ -265,15 +346,25 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
265
346
|
<MessageBubble
|
|
266
347
|
message={msg}
|
|
267
348
|
assistantName={assistantName}
|
|
349
|
+
agentAvatarSeed={agent?.avatarSeed}
|
|
350
|
+
agentName={agent?.name}
|
|
268
351
|
isLast={isLastAssistant}
|
|
269
352
|
onRetry={isLastAssistant ? retryLastMessage : undefined}
|
|
353
|
+
messageIndex={originalIndex >= 0 ? originalIndex : undefined}
|
|
354
|
+
onToggleBookmark={toggleBookmark}
|
|
355
|
+
onEditResend={handleEditResend}
|
|
356
|
+
onFork={handleFork}
|
|
270
357
|
/>
|
|
271
358
|
</div>
|
|
272
359
|
</div>
|
|
273
360
|
)
|
|
274
361
|
})}
|
|
275
|
-
|
|
276
|
-
{streaming &&
|
|
362
|
+
<ApprovalCards agentId={agent?.id} />
|
|
363
|
+
{streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
364
|
+
{streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
365
|
+
{!streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
366
|
+
<SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
|
|
367
|
+
)}
|
|
277
368
|
</div>
|
|
278
369
|
</div>
|
|
279
370
|
{showScrollToBottom && (
|
|
@@ -297,3 +388,16 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
297
388
|
</div>
|
|
298
389
|
)
|
|
299
390
|
}
|
|
391
|
+
|
|
392
|
+
function ApprovalCards({ agentId }: { agentId?: string | null }) {
|
|
393
|
+
const approvals = useApprovalStore((s) => s.approvals)
|
|
394
|
+
const cards = Object.values(approvals).filter((a) => !agentId || a.agentId === agentId)
|
|
395
|
+
if (!cards.length) return null
|
|
396
|
+
return (
|
|
397
|
+
<>
|
|
398
|
+
{cards.map((a) => (
|
|
399
|
+
<ExecApprovalCard key={a.id} approval={a} />
|
|
400
|
+
))}
|
|
401
|
+
</>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
@@ -1,22 +1,72 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMemo } from 'react'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
4
|
import ReactMarkdown from 'react-markdown'
|
|
5
5
|
import remarkGfm from 'remark-gfm'
|
|
6
6
|
import rehypeHighlight from 'rehype-highlight'
|
|
7
7
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
import { CodeBlock } from './code-block'
|
|
9
10
|
import { ToolCallBubble } from './tool-call-bubble'
|
|
10
|
-
import { useChatStore } from '@/stores/use-chat-store'
|
|
11
|
+
import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
|
|
12
|
+
|
|
13
|
+
function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
14
|
+
const [expanded, setExpanded] = useState(false)
|
|
15
|
+
const shouldCollapse = toolEvents.length > 2
|
|
16
|
+
const latestTool = toolEvents[toolEvents.length - 1]
|
|
17
|
+
|
|
18
|
+
if (shouldCollapse && !expanded) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => setExpanded(true)}
|
|
24
|
+
className="self-start flex items-center gap-2 px-3 py-1.5 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] cursor-pointer transition-colors"
|
|
25
|
+
>
|
|
26
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/60">
|
|
27
|
+
<polyline points="6 9 12 15 18 9" />
|
|
28
|
+
</svg>
|
|
29
|
+
<span className="text-[11px] text-text-3 font-mono">
|
|
30
|
+
{toolEvents.length} tool calls
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-[10px] text-text-3/50">
|
|
33
|
+
latest: {latestTool?.name || 'unknown'}
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
42
|
+
{shouldCollapse && (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={() => setExpanded(false)}
|
|
46
|
+
className="self-start px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-[11px] text-text-3 border border-white/[0.06] cursor-pointer transition-colors"
|
|
47
|
+
>
|
|
48
|
+
Collapse tool calls
|
|
49
|
+
</button>
|
|
50
|
+
)}
|
|
51
|
+
{toolEvents.map((event) => (
|
|
52
|
+
<ToolCallBubble key={event.id} event={event} />
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
11
57
|
|
|
12
58
|
interface Props {
|
|
13
59
|
text: string
|
|
14
60
|
assistantName?: string
|
|
61
|
+
agentAvatarSeed?: string
|
|
62
|
+
agentName?: string
|
|
15
63
|
}
|
|
16
64
|
|
|
17
|
-
export function StreamingBubble({ text, assistantName }: Props) {
|
|
65
|
+
export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
|
|
18
66
|
const rendered = useMemo(() => text, [text])
|
|
19
67
|
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
68
|
+
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
69
|
+
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
20
70
|
|
|
21
71
|
return (
|
|
22
72
|
<div
|
|
@@ -24,18 +74,17 @@ export function StreamingBubble({ text, assistantName }: Props) {
|
|
|
24
74
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
25
75
|
>
|
|
26
76
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
27
|
-
<AiAvatar size="sm" />
|
|
77
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
28
78
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
29
79
|
<span className="w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
80
|
+
{streamPhase === 'tool' && streamToolName && (
|
|
81
|
+
<span className="text-[10px] text-text-3/50 font-mono">Using {streamToolName}...</span>
|
|
82
|
+
)}
|
|
30
83
|
</div>
|
|
31
84
|
|
|
32
|
-
{/* Tool call events */}
|
|
85
|
+
{/* Tool call events (collapsible when > 2) */}
|
|
33
86
|
{toolEvents.length > 0 && (
|
|
34
|
-
<
|
|
35
|
-
{toolEvents.map((event) => (
|
|
36
|
-
<ToolCallBubble key={event.id} event={event} />
|
|
37
|
-
))}
|
|
38
|
-
</div>
|
|
87
|
+
<ToolEventsSection toolEvents={toolEvents} />
|
|
39
88
|
)}
|
|
40
89
|
|
|
41
90
|
{rendered && (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Message } from '@/types'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
lastMessage: Message | null
|
|
7
|
+
onSend: (text: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getSuggestions(msg: Message | null): string[] {
|
|
11
|
+
if (!msg?.text) return ['Continue', 'Tell me more']
|
|
12
|
+
const text = msg.text
|
|
13
|
+
|
|
14
|
+
// Error patterns
|
|
15
|
+
if (/error|exception|failed|traceback|panic|ECONNREFUSED|ETIMEDOUT/i.test(text)) {
|
|
16
|
+
return ['Can you fix this?', 'Try an alternative approach', 'Explain the error']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Code blocks present
|
|
20
|
+
if (/```[\s\S]*```/.test(text)) {
|
|
21
|
+
return ['Explain this code', 'Write tests for this', 'Any improvements?']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// File mentions
|
|
25
|
+
if (/`\/[\w./-]+\.\w+`/.test(text) || /\b(created|modified|updated|wrote|saved)\b.*\b(file|files)\b/i.test(text)) {
|
|
26
|
+
return ['Show me the file', 'Make changes to it', 'What else needs updating?']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Task completion signals
|
|
30
|
+
if (/\b(done|complete|finished|ready|all set|successfully)\b/i.test(text)) {
|
|
31
|
+
return ["What's next?", 'Summarize what was done', 'Any remaining issues?']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Question asked by assistant
|
|
35
|
+
if (/\?\s*$/.test(text.trim())) {
|
|
36
|
+
return ['Yes, go ahead', 'No, try a different approach', 'Tell me more about the options']
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// List/steps presented
|
|
40
|
+
if (/^\s*(\d+\.|[-*])\s/m.test(text)) {
|
|
41
|
+
return ['Start with the first step', 'Can you elaborate?', 'Any alternatives?']
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ['Continue', 'Tell me more', 'Can you explain further?']
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function SuggestionsBar({ lastMessage, onSend }: Props) {
|
|
48
|
+
const suggestions = lastMessage?.suggestions?.length === 3
|
|
49
|
+
? lastMessage.suggestions
|
|
50
|
+
: getSuggestions(lastMessage)
|
|
51
|
+
|
|
52
|
+
if (!suggestions.length) return null
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className="flex flex-wrap gap-2 px-1 pt-2"
|
|
57
|
+
style={{ animation: 'fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
58
|
+
>
|
|
59
|
+
{suggestions.map((text) => (
|
|
60
|
+
<button
|
|
61
|
+
key={text}
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => onSend(text)}
|
|
64
|
+
className="rounded-full px-3.5 py-1.5 text-[12px] font-500 border border-white/[0.06] bg-white/[0.03]
|
|
65
|
+
text-text-3 hover:text-text-2 hover:bg-white/[0.06] hover:border-white/[0.10]
|
|
66
|
+
cursor-pointer transition-all active:scale-[0.97]"
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
{text}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
4
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
assistantName?: string
|
|
9
|
+
agentAvatarSeed?: string
|
|
10
|
+
agentName?: string
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
export function ThinkingIndicator({ assistantName }: Props) {
|
|
13
|
+
export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }: Props) {
|
|
14
|
+
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
15
|
+
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
16
|
+
|
|
17
|
+
const statusText = streamPhase === 'tool' && streamToolName
|
|
18
|
+
? `Using ${streamToolName}...`
|
|
19
|
+
: 'Thinking...'
|
|
20
|
+
|
|
10
21
|
return (
|
|
11
22
|
<div className="flex flex-col items-start"
|
|
12
23
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
13
24
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
14
|
-
<AiAvatar size="sm" />
|
|
25
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
15
26
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
16
27
|
</div>
|
|
17
28
|
<div className="bubble-ai px-6 py-5">
|
|
18
|
-
<div className="flex gap-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
<div className="flex items-center gap-3">
|
|
30
|
+
<div className="flex gap-2">
|
|
31
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
32
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
33
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
34
|
+
</div>
|
|
35
|
+
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
22
36
|
</div>
|
|
23
37
|
</div>
|
|
24
38
|
</div>
|