@swarmclawai/swarmclaw 0.6.0 → 0.6.3
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 +56 -42
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +16 -35
- package/src/app/api/tts/stream/route.ts +14 -42
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +76 -24
- package/src/components/chat/chat-header.tsx +522 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +113 -8
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +84 -17
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +125 -14
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +594 -16
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +182 -8
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +583 -63
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/file.ts +26 -7
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +118 -8
- package/src/lib/server/stream-agent-chat.ts +39 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -14,6 +14,7 @@ import { ExecApprovalCard } from './exec-approval-card'
|
|
|
14
14
|
import { HeartbeatMoment, ActivityMoment, isNotableTool } from './activity-moment'
|
|
15
15
|
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
16
16
|
import { useWs } from '@/hooks/use-ws'
|
|
17
|
+
import { GatewayDisconnectOverlay, useGatewayStatus } from '@/components/settings/gateway-disconnect-overlay'
|
|
17
18
|
|
|
18
19
|
const INTRO_GREETINGS = [
|
|
19
20
|
'What can I help you with?',
|
|
@@ -45,9 +46,10 @@ function dateSeparator(ts: number): string {
|
|
|
45
46
|
interface Props {
|
|
46
47
|
messages: Message[]
|
|
47
48
|
streaming: boolean
|
|
49
|
+
connectorFilter?: string | null
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
export function MessageList({ messages, streaming }: Props) {
|
|
52
|
+
export function MessageList({ messages, streaming, connectorFilter = null }: Props) {
|
|
51
53
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
52
54
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
53
55
|
const snapUntilRef = useRef(0)
|
|
@@ -57,6 +59,10 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
57
59
|
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
58
60
|
const editAndResend = useChatStore((s) => s.editAndResend)
|
|
59
61
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
62
|
+
const hasMoreMessages = useChatStore((s) => s.hasMoreMessages)
|
|
63
|
+
const loadingMore = useChatStore((s) => s.loadingMore)
|
|
64
|
+
const totalMessages = useChatStore((s) => s.totalMessages)
|
|
65
|
+
const loadMoreMessages = useChatStore((s) => s.loadMoreMessages)
|
|
60
66
|
const forkSession = useAppStore((s) => s.forkSession)
|
|
61
67
|
const session = useAppStore((s) => {
|
|
62
68
|
const id = s.currentSessionId
|
|
@@ -73,6 +79,11 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
73
79
|
const showOk = appSettings.heartbeatShowOk ?? false
|
|
74
80
|
const showAlerts = appSettings.heartbeatShowAlerts ?? true
|
|
75
81
|
|
|
82
|
+
// Gateway disconnect overlay for openclaw agents
|
|
83
|
+
const isOpenClaw = agent?.provider === 'openclaw'
|
|
84
|
+
const gatewayStatus = useGatewayStatus()
|
|
85
|
+
const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
|
|
86
|
+
|
|
76
87
|
// Moment overlay for last assistant message (heartbeat or tool events)
|
|
77
88
|
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
|
|
78
89
|
const [currentMoment, setCurrentMoment] = useState<MomentType | null>(null)
|
|
@@ -108,6 +119,8 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
108
119
|
// Bookmark filter
|
|
109
120
|
const [bookmarkFilter, setBookmarkFilter] = useState(false)
|
|
110
121
|
|
|
122
|
+
// Connector filtering is handled via connectorFilter prop from chat-area
|
|
123
|
+
|
|
111
124
|
const toggleBookmark = useCallback(async (index: number) => {
|
|
112
125
|
if (!sessionId) return
|
|
113
126
|
const msg = messages[index]
|
|
@@ -166,10 +179,13 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
// Apply bookmark filter
|
|
170
|
-
|
|
182
|
+
// Apply bookmark + connector filter
|
|
183
|
+
let filteredMessages = bookmarkFilter
|
|
171
184
|
? displayedMessages.filter((msg) => msg.bookmarked)
|
|
172
185
|
: displayedMessages
|
|
186
|
+
if (connectorFilter) {
|
|
187
|
+
filteredMessages = filteredMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
|
|
188
|
+
}
|
|
173
189
|
|
|
174
190
|
// Search matches
|
|
175
191
|
const searchMatches = searchQuery.trim()
|
|
@@ -274,6 +290,23 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
274
290
|
return () => window.removeEventListener('swarmclaw:scroll-bottom', handler)
|
|
275
291
|
}, [handleScrollToBottom])
|
|
276
292
|
|
|
293
|
+
// Scroll to a specific message by index (used by search)
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (typeof window === 'undefined') return
|
|
296
|
+
const handler = (e: Event) => {
|
|
297
|
+
const idx = (e as CustomEvent).detail?.index
|
|
298
|
+
if (typeof idx !== 'number') return
|
|
299
|
+
const el = scrollRef.current?.querySelector(`[data-message-index="${idx}"]`) as HTMLElement | null
|
|
300
|
+
if (el) {
|
|
301
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
302
|
+
el.classList.add('bg-accent-bright/10')
|
|
303
|
+
setTimeout(() => el.classList.remove('bg-accent-bright/10'), 2000)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
window.addEventListener('swarmclaw:scroll-to-message', handler)
|
|
307
|
+
return () => window.removeEventListener('swarmclaw:scroll-to-message', handler)
|
|
308
|
+
}, [])
|
|
309
|
+
|
|
277
310
|
// Ctrl+F search toggle
|
|
278
311
|
useEffect(() => {
|
|
279
312
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -296,7 +329,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
296
329
|
}, [searchOpen])
|
|
297
330
|
|
|
298
331
|
return (
|
|
299
|
-
<div className="relative flex-1 min-h-0 min-w-0">
|
|
332
|
+
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
|
|
300
333
|
{/* In-thread search bar */}
|
|
301
334
|
{searchOpen && (
|
|
302
335
|
<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]">
|
|
@@ -310,6 +343,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
310
343
|
value={searchQuery}
|
|
311
344
|
onChange={(e) => { setSearchQuery(e.target.value); setSearchIdx(0) }}
|
|
312
345
|
placeholder="Search in conversation..."
|
|
346
|
+
aria-label="Search messages"
|
|
313
347
|
className="flex-1 bg-transparent text-text text-[13px] outline-none placeholder:text-text-3/50"
|
|
314
348
|
style={{ fontFamily: 'inherit' }}
|
|
315
349
|
onKeyDown={(e) => {
|
|
@@ -344,7 +378,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
344
378
|
<button
|
|
345
379
|
onClick={() => setBookmarkFilter((v) => !v)}
|
|
346
380
|
aria-label={bookmarkFilter ? 'Show all messages' : 'Show bookmarked only'}
|
|
347
|
-
className={`p-1 rounded-[6px] hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors ${bookmarkFilter ? 'text-
|
|
381
|
+
className={`p-1 rounded-[6px] hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors ${bookmarkFilter ? 'text-amber-500' : 'text-text-3 hover:text-text-2'}`}
|
|
348
382
|
>
|
|
349
383
|
<svg width="14" height="14" viewBox="0 0 24 24" fill={bookmarkFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
350
384
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
@@ -363,11 +397,46 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
363
397
|
<div
|
|
364
398
|
ref={scrollRef}
|
|
365
399
|
onScroll={updateScrollState}
|
|
366
|
-
className="h-
|
|
400
|
+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-6 md:px-12 lg:px-16 pt-6 pb-10 fade-up"
|
|
367
401
|
>
|
|
368
402
|
<div className="flex flex-col gap-6 relative">
|
|
369
403
|
{/* Chat spine — vertical line for assistant messages */}
|
|
370
404
|
<div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06] pointer-events-none" />
|
|
405
|
+
{hasMoreMessages && (
|
|
406
|
+
<div className="flex justify-center py-3">
|
|
407
|
+
<button
|
|
408
|
+
onClick={async () => {
|
|
409
|
+
const el = scrollRef.current
|
|
410
|
+
const prevHeight = el?.scrollHeight ?? 0
|
|
411
|
+
await loadMoreMessages()
|
|
412
|
+
// Preserve scroll position after prepending
|
|
413
|
+
if (el) {
|
|
414
|
+
requestAnimationFrame(() => {
|
|
415
|
+
el.scrollTop += el.scrollHeight - prevHeight
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
}}
|
|
419
|
+
disabled={loadingMore}
|
|
420
|
+
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-white/[0.08] bg-surface/80 text-text-3 text-[12px] font-600 hover:bg-surface-2 hover:text-text-2 transition-colors cursor-pointer disabled:opacity-50"
|
|
421
|
+
>
|
|
422
|
+
{loadingMore ? (
|
|
423
|
+
<>
|
|
424
|
+
<span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-text-2 animate-spin" />
|
|
425
|
+
Loading...
|
|
426
|
+
</>
|
|
427
|
+
) : (
|
|
428
|
+
<>
|
|
429
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
|
430
|
+
<path d="M12 19V5" />
|
|
431
|
+
<path d="m5 12 7-7 7 7" />
|
|
432
|
+
</svg>
|
|
433
|
+
Load earlier messages
|
|
434
|
+
<span className="text-text-3/50">({totalMessages - messages.length} more)</span>
|
|
435
|
+
</>
|
|
436
|
+
)}
|
|
437
|
+
</button>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
371
440
|
{filteredMessages.length === 0 && !streaming && (
|
|
372
441
|
<div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
|
|
373
442
|
<AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
|
|
@@ -378,6 +447,41 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
378
447
|
</div>
|
|
379
448
|
)}
|
|
380
449
|
{filteredMessages.map((msg, i) => {
|
|
450
|
+
// Context-clear divider — render a visual separator instead of a bubble
|
|
451
|
+
if (msg.kind === 'context-clear') {
|
|
452
|
+
const originalIndex = messages.indexOf(msg)
|
|
453
|
+
return (
|
|
454
|
+
<div key={`ctx-clear-${msg.time}-${i}`} className="group/ctx flex items-center gap-4 py-3">
|
|
455
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
456
|
+
<span className="flex items-center gap-1.5 text-[10px] font-600 text-amber-400/60 uppercase tracking-[0.1em]">
|
|
457
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="shrink-0">
|
|
458
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
459
|
+
<polyline points="8 8 4 12 8 16" />
|
|
460
|
+
<polyline points="16 8 20 12 16 16" />
|
|
461
|
+
</svg>
|
|
462
|
+
New context
|
|
463
|
+
{msg.time ? ` · ${new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''}
|
|
464
|
+
</span>
|
|
465
|
+
{sessionId && originalIndex >= 0 && (
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
onClick={async () => {
|
|
469
|
+
try {
|
|
470
|
+
await api('DELETE', `/sessions/${sessionId}/messages`, { messageIndex: originalIndex })
|
|
471
|
+
setMessages(messages.filter((_: Message, idx: number) => idx !== originalIndex))
|
|
472
|
+
} catch { /* best-effort */ }
|
|
473
|
+
}}
|
|
474
|
+
className="opacity-0 group-hover/ctx:opacity-100 text-[10px] font-600 text-amber-400/60 hover:text-amber-400 bg-transparent border-none cursor-pointer transition-all px-1.5 py-0.5 rounded-[4px] hover:bg-amber-400/10"
|
|
475
|
+
title="Undo — restore full context"
|
|
476
|
+
>
|
|
477
|
+
Undo
|
|
478
|
+
</button>
|
|
479
|
+
)}
|
|
480
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
481
|
+
</div>
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
381
485
|
// Find original index in the full messages array for API calls
|
|
382
486
|
const originalIndex = messages.indexOf(msg)
|
|
383
487
|
const isLastAssistant = msg.role === 'assistant' && !streaming
|
|
@@ -407,7 +511,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
407
511
|
}
|
|
408
512
|
|
|
409
513
|
return (
|
|
410
|
-
<div key={`${msg.time}-${i}`}>
|
|
514
|
+
<div key={`${msg.time}-${i}`} data-message-index={i}>
|
|
411
515
|
{showDateSep && (
|
|
412
516
|
<div className="flex items-center gap-4 py-2 mb-2">
|
|
413
517
|
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
@@ -438,11 +542,12 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
438
542
|
<ApprovalCards agentId={agent?.id} />
|
|
439
543
|
{streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
440
544
|
{streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
441
|
-
{!streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
545
|
+
{appSettings.suggestionsEnabled !== false && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
442
546
|
<SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
|
|
443
547
|
)}
|
|
444
548
|
</div>
|
|
445
549
|
</div>
|
|
550
|
+
{showGatewayOverlay && <GatewayDisconnectOverlay />}
|
|
446
551
|
{showScrollToBottom && (
|
|
447
552
|
<button
|
|
448
553
|
onClick={handleScrollToBottom}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
4
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
5
5
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
-
import { ToolCallBubble } from './tool-call-bubble'
|
|
6
|
+
import { ToolCallBubble, extractMedia } from './tool-call-bubble'
|
|
7
7
|
import { ActivityMoment, isNotableTool } from './activity-moment'
|
|
8
8
|
import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
|
|
9
9
|
import { isStructuredMarkdown } from './markdown-utils'
|
|
@@ -13,6 +13,26 @@ function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
|
13
13
|
const shouldCollapse = toolEvents.length > 2
|
|
14
14
|
const latestTool = toolEvents[toolEvents.length - 1]
|
|
15
15
|
|
|
16
|
+
// When collapsed, collect deduplicated media from all tool events so files remain visible
|
|
17
|
+
const collapsedMedia = useMemo(() => {
|
|
18
|
+
if (!shouldCollapse || expanded) return null
|
|
19
|
+
const seen = new Set<string>()
|
|
20
|
+
const images: string[] = []
|
|
21
|
+
const videos: string[] = []
|
|
22
|
+
const pdfs: { name: string; url: string }[] = []
|
|
23
|
+
const files: { name: string; url: string }[] = []
|
|
24
|
+
for (const ev of toolEvents) {
|
|
25
|
+
if (!ev.output) continue
|
|
26
|
+
const m = extractMedia(ev.output)
|
|
27
|
+
for (const url of m.images) { if (!seen.has(url)) { seen.add(url); images.push(url) } }
|
|
28
|
+
for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
|
|
29
|
+
for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
|
|
30
|
+
for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
|
|
31
|
+
}
|
|
32
|
+
if (!images.length && !videos.length && !pdfs.length && !files.length) return null
|
|
33
|
+
return { images, videos, pdfs, files }
|
|
34
|
+
}, [toolEvents, shouldCollapse, expanded])
|
|
35
|
+
|
|
16
36
|
if (shouldCollapse && !expanded) {
|
|
17
37
|
return (
|
|
18
38
|
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
@@ -31,6 +51,33 @@ function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
|
31
51
|
latest: {latestTool?.name || 'unknown'}
|
|
32
52
|
</span>
|
|
33
53
|
</button>
|
|
54
|
+
{collapsedMedia && (
|
|
55
|
+
<>
|
|
56
|
+
{collapsedMedia.images.map((src, i) => (
|
|
57
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
58
|
+
<img key={`ci-${i}`} src={src} alt={`Screenshot ${i + 1}`} loading="lazy"
|
|
59
|
+
className="max-w-[400px] rounded-[10px] border border-white/10"
|
|
60
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
61
|
+
))}
|
|
62
|
+
{collapsedMedia.videos.map((src, i) => (
|
|
63
|
+
<video key={`cv-${i}`} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
64
|
+
))}
|
|
65
|
+
{collapsedMedia.pdfs.map((file, i) => (
|
|
66
|
+
<div key={`cp-${i}`} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
67
|
+
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
{collapsedMedia.files.map((file, i) => (
|
|
71
|
+
<a key={`cf-${i}`} href={file.url} download className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/10 bg-surface/60 text-[13px] text-text-2 no-underline">
|
|
72
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
73
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
74
|
+
<polyline points="14 2 14 8 20 8" />
|
|
75
|
+
</svg>
|
|
76
|
+
{file.name}
|
|
77
|
+
</a>
|
|
78
|
+
))}
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
34
81
|
</div>
|
|
35
82
|
)
|
|
36
83
|
}
|
|
@@ -64,6 +111,7 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
|
|
|
64
111
|
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
65
112
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
66
113
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
114
|
+
const thinkingText = useChatStore((s) => s.thinkingText)
|
|
67
115
|
const wide = useMemo(() => isStructuredMarkdown(text), [text])
|
|
68
116
|
|
|
69
117
|
// Track which activity moments have been dismissed
|
|
@@ -107,6 +155,25 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
|
|
|
107
155
|
)}
|
|
108
156
|
</div>
|
|
109
157
|
|
|
158
|
+
{/* Collapsed thinking section (shown when text has started but thinking exists) */}
|
|
159
|
+
{text && thinkingText && (
|
|
160
|
+
<div className="max-w-[85%] md:max-w-[72%] mb-2">
|
|
161
|
+
<details className="group rounded-[12px] border border-purple-500/15 bg-purple-500/[0.04]">
|
|
162
|
+
<summary className="flex items-center gap-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
|
|
163
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-purple-400/60 shrink-0 transition-transform group-open:rotate-90">
|
|
164
|
+
<polyline points="9 18 15 12 9 6" />
|
|
165
|
+
</svg>
|
|
166
|
+
<span className="text-[11px] font-600 text-purple-400/70 uppercase tracking-[0.05em]">Thinking</span>
|
|
167
|
+
</summary>
|
|
168
|
+
<div className="px-3.5 pb-3 pt-1 max-h-[300px] overflow-y-auto">
|
|
169
|
+
<div className="text-[13px] leading-[1.6] text-text-3/70 whitespace-pre-wrap break-words">
|
|
170
|
+
{thinkingText}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</details>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
110
177
|
{/* Tool call events (collapsible when > 2) */}
|
|
111
178
|
{toolEvents.length > 0 && (
|
|
112
179
|
<ToolEventsSection toolEvents={toolEvents} />
|
|
@@ -20,6 +20,7 @@ const TOOL_COLORS: Record<string, string> = {
|
|
|
20
20
|
web_search: '#3B82F6',
|
|
21
21
|
web_fetch: '#3B82F6',
|
|
22
22
|
delegate_to_agent: '#6366F1',
|
|
23
|
+
check_delegation_status: '#6366F1',
|
|
23
24
|
delegate_to_claude_code: '#6366F1',
|
|
24
25
|
delegate_to_codex_cli: '#0EA5E9',
|
|
25
26
|
delegate_to_opencode_cli: '#14B8A6',
|
|
@@ -71,6 +72,7 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
71
72
|
codex_cli: 'Codex CLI',
|
|
72
73
|
opencode_cli: 'OpenCode CLI',
|
|
73
74
|
delegate_to_agent: 'Agent Delegation',
|
|
75
|
+
check_delegation_status: 'Check Delegation',
|
|
74
76
|
delegate_to_claude_code: 'Claude Code',
|
|
75
77
|
delegate_to_codex_cli: 'Codex CLI',
|
|
76
78
|
delegate_to_opencode_cli: 'OpenCode CLI',
|
|
@@ -107,6 +109,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
107
109
|
codex_cli: 'Enable delegation to OpenAI Codex CLI',
|
|
108
110
|
opencode_cli: 'Enable delegation to OpenCode CLI',
|
|
109
111
|
delegate_to_agent: 'Delegate a task to another agent',
|
|
112
|
+
check_delegation_status: 'Check the status of a delegated task',
|
|
110
113
|
delegate_to_claude_code: 'Delegate complex coding tasks to Claude Code',
|
|
111
114
|
delegate_to_codex_cli: 'Delegate complex coding tasks to Codex CLI',
|
|
112
115
|
delegate_to_opencode_cli: 'Delegate complex coding tasks to OpenCode CLI',
|
|
@@ -164,6 +167,45 @@ function formatJson(raw: string): string {
|
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
/** Relative time label like "2h ago", "5m ago" */
|
|
171
|
+
function relativeTime(ts: number): string {
|
|
172
|
+
const diff = Date.now() - ts
|
|
173
|
+
if (diff < 60_000) return 'just now'
|
|
174
|
+
if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`
|
|
175
|
+
if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`
|
|
176
|
+
return `${Math.round(diff / 86_400_000)}d ago`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Format search_history_tool output into human-readable text */
|
|
180
|
+
function formatSearchHistoryOutput(raw: string): string | null {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(raw)
|
|
183
|
+
const matches = parsed?.matches
|
|
184
|
+
if (!Array.isArray(matches) || matches.length === 0) {
|
|
185
|
+
return parsed?.query ? `No matches found for "${parsed.query}"` : null
|
|
186
|
+
}
|
|
187
|
+
const header = `Found ${matches.length} match${matches.length === 1 ? '' : 'es'}${parsed.query ? ` for "${parsed.query}"` : ''}`
|
|
188
|
+
const lines = matches.slice(0, 10).map((m: Record<string, unknown>, i: number) => {
|
|
189
|
+
const role = String(m.role || 'unknown')
|
|
190
|
+
const kind = m.kind ? ` (${m.kind})` : ''
|
|
191
|
+
const time = typeof m.time === 'number' ? ` · ${relativeTime(m.time)}` : ''
|
|
192
|
+
const text = String(m.text || '').replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
193
|
+
return `${i + 1}. [${role}]${kind}${time}\n ${text}${String(m.text || '').length > 200 ? '...' : ''}`
|
|
194
|
+
})
|
|
195
|
+
return `${header}\n\n${lines.join('\n\n')}`
|
|
196
|
+
} catch {
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Try to produce a human-readable output for known tool types */
|
|
202
|
+
function formatToolOutput(toolName: string, raw: string): string {
|
|
203
|
+
if (toolName === 'search_history_tool') {
|
|
204
|
+
return formatSearchHistoryOutput(raw) || formatJson(raw)
|
|
205
|
+
}
|
|
206
|
+
return formatJson(raw)
|
|
207
|
+
}
|
|
208
|
+
|
|
167
209
|
/** Extract a human-readable preview from tool input */
|
|
168
210
|
function getInputPreview(name: string, input: string): string {
|
|
169
211
|
try {
|
|
@@ -218,7 +260,7 @@ function getInputPreview(name: string, input: string): string {
|
|
|
218
260
|
}
|
|
219
261
|
|
|
220
262
|
/** Extract embedded images, videos, PDFs, and file links from tool output */
|
|
221
|
-
function extractMedia(output: string): { images: string[]; videos: string[]; pdfs: { name: string; url: string }[]; files: { name: string; url: string }[]; cleanText: string } {
|
|
263
|
+
export function extractMedia(output: string): { images: string[]; videos: string[]; pdfs: { name: string; url: string }[]; files: { name: string; url: string }[]; cleanText: string } {
|
|
222
264
|
const images: string[] = []
|
|
223
265
|
const videos: string[] = []
|
|
224
266
|
const pdfs: { name: string; url: string }[] = []
|
|
@@ -341,8 +383,8 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
341
383
|
|
|
342
384
|
const formattedCleanOutput = useMemo(() => {
|
|
343
385
|
if (!media.cleanText) return ''
|
|
344
|
-
return
|
|
345
|
-
}, [media.cleanText])
|
|
386
|
+
return formatToolOutput(event.name, media.cleanText)
|
|
387
|
+
}, [event.name, media.cleanText])
|
|
346
388
|
|
|
347
389
|
const hasMedia = media.images.length > 0 || media.videos.length > 0 || media.pdfs.length > 0 || media.files.length > 0
|
|
348
390
|
|
|
@@ -30,7 +30,7 @@ export function TransferAgentPicker({ excludeIds, filterIds, onSelect, onClose }
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
32
|
<div className="fixed inset-0 z-40" onClick={onClose} />
|
|
33
|
-
<div className="absolute left-0 bottom-full mb-2 z-50 w-[220px] rounded-[10px] bg-
|
|
33
|
+
<div className="absolute left-0 bottom-full mb-2 z-50 w-[220px] rounded-[10px] bg-surface/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
|
34
34
|
<div className="p-2">
|
|
35
35
|
<input
|
|
36
36
|
value={query}
|
|
@@ -24,6 +24,13 @@ export function ChatroomList() {
|
|
|
24
24
|
useEffect(() => { refresh() }, [refresh])
|
|
25
25
|
useWs('chatrooms', refresh, 15_000)
|
|
26
26
|
|
|
27
|
+
// Auto-select the latest chatroom when none is selected
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (currentChatroomId) return
|
|
30
|
+
const latest = Object.values(chatrooms).sort((a, b) => b.updatedAt - a.updatedAt)[0]
|
|
31
|
+
if (latest) setCurrentChatroom(latest.id)
|
|
32
|
+
}, [chatrooms, currentChatroomId, setCurrentChatroom])
|
|
33
|
+
|
|
27
34
|
const [filter, setFilter] = useState<'all' | 'active' | 'recent'>('all')
|
|
28
35
|
|
|
29
36
|
const sorted = useMemo(() =>
|
|
@@ -63,7 +70,7 @@ export function ChatroomList() {
|
|
|
63
70
|
type="button"
|
|
64
71
|
onClick={() => setFilter(f)}
|
|
65
72
|
data-active={filter === f || undefined}
|
|
66
|
-
className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 border-none cursor-pointer transition-all
|
|
73
|
+
className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 border-none cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
|
|
67
74
|
data-[active]:bg-accent-soft data-[active]:text-accent-bright
|
|
68
75
|
bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
|
|
69
76
|
>
|
|
@@ -14,6 +14,7 @@ import { AgentHoverCard } from './agent-hover-card'
|
|
|
14
14
|
import { ChatroomToolRequestBanner } from './chatroom-tool-request-banner'
|
|
15
15
|
import { isStructuredMarkdown } from '@/components/chat/markdown-utils'
|
|
16
16
|
import { TransferAgentPicker } from '@/components/chat/transfer-agent-picker'
|
|
17
|
+
import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
|
|
17
18
|
import type { ChatroomMessage, Agent } from '@/types'
|
|
18
19
|
|
|
19
20
|
interface Props {
|
|
@@ -188,13 +189,17 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
188
189
|
<div className="flex items-baseline gap-2 mb-0.5">
|
|
189
190
|
{!isUser && agent ? (
|
|
190
191
|
<AgentHoverCard agent={agent}>
|
|
191
|
-
<span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer">
|
|
192
|
+
<span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer flex items-center gap-1.5">
|
|
193
|
+
{message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
|
|
192
194
|
{message.senderName}
|
|
193
195
|
</span>
|
|
194
196
|
</AgentHoverCard>
|
|
195
197
|
) : (
|
|
196
|
-
<span className="text-[13px] font-600 text-text">
|
|
197
|
-
{message.
|
|
198
|
+
<span className="text-[13px] font-600 text-text flex items-center gap-1.5">
|
|
199
|
+
{message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
|
|
200
|
+
{isUser && message.source?.senderName
|
|
201
|
+
? `${message.source.senderName} via ${CONNECTOR_PLATFORM_META[message.source.platform]?.label || message.source.platform}`
|
|
202
|
+
: message.senderName}
|
|
198
203
|
</span>
|
|
199
204
|
)}
|
|
200
205
|
<span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
|
|
@@ -177,7 +177,7 @@ export function ChatroomView() {
|
|
|
177
177
|
</svg>
|
|
178
178
|
</div>
|
|
179
179
|
<div className="flex-1 min-w-0">
|
|
180
|
-
<h3 className="text-[14px] font-
|
|
180
|
+
<h3 className="text-[14px] font-700 text-text truncate">{chatroom.name}</h3>
|
|
181
181
|
<p className="text-[11px] text-text-3 truncate">
|
|
182
182
|
{memberAgents.length} agent{memberAgents.length !== 1 ? 's' : ''}
|
|
183
183
|
{chatroom.description ? ` · ${chatroom.description}` : ''}
|
|
@@ -192,7 +192,7 @@ export function ChatroomView() {
|
|
|
192
192
|
onClick={() => navigateToAgent(agent.id)}
|
|
193
193
|
className="relative transition-all duration-200 hover:scale-110 hover:z-10 hover:-translate-y-0.5 cursor-pointer bg-transparent border-none p-0"
|
|
194
194
|
>
|
|
195
|
-
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={22}
|
|
195
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
|
|
196
196
|
</button>
|
|
197
197
|
</TooltipTrigger>
|
|
198
198
|
<TooltipContent side="bottom" sideOffset={6}>
|
|
@@ -201,7 +201,7 @@ export function ChatroomView() {
|
|
|
201
201
|
</Tooltip>
|
|
202
202
|
))}
|
|
203
203
|
{memberAgents.length > 5 && (
|
|
204
|
-
<div className="w-[22px] h-[22px] rounded-full bg-white/[0.08] flex items-center justify-center text-[9px] text-text-3
|
|
204
|
+
<div className="w-[22px] h-[22px] rounded-full bg-white/[0.08] flex items-center justify-center text-[9px] text-text-3">
|
|
205
205
|
+{memberAgents.length - 5}
|
|
206
206
|
</div>
|
|
207
207
|
)}
|