@swarmclawai/swarmclaw 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- 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 +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- 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 +46 -22
- package/src/components/chat/chat-header.tsx +455 -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 +180 -7
- 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 +68 -16
- 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 +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- 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 +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- 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/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 +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -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
|
@@ -6,6 +6,7 @@ import { useChatStore } from '@/stores/use-chat-store'
|
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { api } from '@/lib/api-client'
|
|
8
8
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
+
import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
|
|
9
10
|
import { MessageBubble } from './message-bubble'
|
|
10
11
|
import { StreamingBubble } from './streaming-bubble'
|
|
11
12
|
import { ThinkingIndicator } from './thinking-indicator'
|
|
@@ -14,6 +15,7 @@ import { ExecApprovalCard } from './exec-approval-card'
|
|
|
14
15
|
import { HeartbeatMoment, ActivityMoment, isNotableTool } from './activity-moment'
|
|
15
16
|
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
16
17
|
import { useWs } from '@/hooks/use-ws'
|
|
18
|
+
import { GatewayDisconnectOverlay, useGatewayStatus } from '@/components/settings/gateway-disconnect-overlay'
|
|
17
19
|
|
|
18
20
|
const INTRO_GREETINGS = [
|
|
19
21
|
'What can I help you with?',
|
|
@@ -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,10 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
108
119
|
// Bookmark filter
|
|
109
120
|
const [bookmarkFilter, setBookmarkFilter] = useState(false)
|
|
110
121
|
|
|
122
|
+
// Connector source filter
|
|
123
|
+
const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
|
|
124
|
+
const [connectorFilterCollapsed, setConnectorFilterCollapsed] = useState(false)
|
|
125
|
+
|
|
111
126
|
const toggleBookmark = useCallback(async (index: number) => {
|
|
112
127
|
if (!sessionId) return
|
|
113
128
|
const msg = messages[index]
|
|
@@ -166,10 +181,24 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
|
|
169
|
-
//
|
|
170
|
-
const
|
|
184
|
+
// Collect unique connector sources for filter UI
|
|
185
|
+
const connectorSources = new Map<string, { platform: string; connectorName: string }>()
|
|
186
|
+
for (const msg of displayedMessages) {
|
|
187
|
+
if (msg.source?.connectorId && !connectorSources.has(msg.source.connectorId)) {
|
|
188
|
+
connectorSources.set(msg.source.connectorId, {
|
|
189
|
+
platform: msg.source.platform,
|
|
190
|
+
connectorName: msg.source.connectorName,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Apply bookmark + connector filter
|
|
196
|
+
let filteredMessages = bookmarkFilter
|
|
171
197
|
? displayedMessages.filter((msg) => msg.bookmarked)
|
|
172
198
|
: displayedMessages
|
|
199
|
+
if (connectorFilter) {
|
|
200
|
+
filteredMessages = filteredMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
|
|
201
|
+
}
|
|
173
202
|
|
|
174
203
|
// Search matches
|
|
175
204
|
const searchMatches = searchQuery.trim()
|
|
@@ -274,6 +303,23 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
274
303
|
return () => window.removeEventListener('swarmclaw:scroll-bottom', handler)
|
|
275
304
|
}, [handleScrollToBottom])
|
|
276
305
|
|
|
306
|
+
// Scroll to a specific message by index (used by search)
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (typeof window === 'undefined') return
|
|
309
|
+
const handler = (e: Event) => {
|
|
310
|
+
const idx = (e as CustomEvent).detail?.index
|
|
311
|
+
if (typeof idx !== 'number') return
|
|
312
|
+
const el = scrollRef.current?.querySelector(`[data-message-index="${idx}"]`) as HTMLElement | null
|
|
313
|
+
if (el) {
|
|
314
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
315
|
+
el.classList.add('bg-accent-bright/10')
|
|
316
|
+
setTimeout(() => el.classList.remove('bg-accent-bright/10'), 2000)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
window.addEventListener('swarmclaw:scroll-to-message', handler)
|
|
320
|
+
return () => window.removeEventListener('swarmclaw:scroll-to-message', handler)
|
|
321
|
+
}, [])
|
|
322
|
+
|
|
277
323
|
// Ctrl+F search toggle
|
|
278
324
|
useEffect(() => {
|
|
279
325
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -296,7 +342,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
296
342
|
}, [searchOpen])
|
|
297
343
|
|
|
298
344
|
return (
|
|
299
|
-
<div className="relative flex-1 min-h-0 min-w-0">
|
|
345
|
+
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
|
|
300
346
|
{/* In-thread search bar */}
|
|
301
347
|
{searchOpen && (
|
|
302
348
|
<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 +356,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
310
356
|
value={searchQuery}
|
|
311
357
|
onChange={(e) => { setSearchQuery(e.target.value); setSearchIdx(0) }}
|
|
312
358
|
placeholder="Search in conversation..."
|
|
359
|
+
aria-label="Search messages"
|
|
313
360
|
className="flex-1 bg-transparent text-text text-[13px] outline-none placeholder:text-text-3/50"
|
|
314
361
|
style={{ fontFamily: 'inherit' }}
|
|
315
362
|
onKeyDown={(e) => {
|
|
@@ -344,7 +391,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
344
391
|
<button
|
|
345
392
|
onClick={() => setBookmarkFilter((v) => !v)}
|
|
346
393
|
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-
|
|
394
|
+
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
395
|
>
|
|
349
396
|
<svg width="14" height="14" viewBox="0 0 24 24" fill={bookmarkFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
350
397
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
@@ -360,14 +407,104 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
360
407
|
</div>
|
|
361
408
|
)}
|
|
362
409
|
|
|
410
|
+
{/* Connector source filter — shown when connector messages exist */}
|
|
411
|
+
{connectorSources.size > 0 && (
|
|
412
|
+
<div className="flex items-center gap-1.5 px-6 md:px-12 lg:px-16 py-1.5 border-b border-white/[0.04]">
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => setConnectorFilterCollapsed((c) => !c)}
|
|
415
|
+
className="flex items-center gap-1 text-[10px] text-text-3/50 uppercase tracking-wider font-600 mr-1 bg-transparent border-none cursor-pointer hover:text-text-3/70 transition-colors p-0"
|
|
416
|
+
title={connectorFilterCollapsed ? 'Expand source filter' : 'Collapse source filter'}
|
|
417
|
+
>
|
|
418
|
+
<svg
|
|
419
|
+
width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
|
|
420
|
+
className="transition-transform duration-200"
|
|
421
|
+
style={{ transform: connectorFilterCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)' }}
|
|
422
|
+
>
|
|
423
|
+
<polyline points="6 9 12 15 18 9" />
|
|
424
|
+
</svg>
|
|
425
|
+
Source
|
|
426
|
+
{connectorFilterCollapsed && connectorFilter && (
|
|
427
|
+
<span className="text-accent-bright/70 normal-case tracking-normal">
|
|
428
|
+
({connectorSources.get(connectorFilter)?.connectorName || connectorFilter})
|
|
429
|
+
</span>
|
|
430
|
+
)}
|
|
431
|
+
</button>
|
|
432
|
+
{!connectorFilterCollapsed && (
|
|
433
|
+
<>
|
|
434
|
+
<button
|
|
435
|
+
onClick={() => setConnectorFilter(null)}
|
|
436
|
+
className={`px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
|
|
437
|
+
!connectorFilter ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
|
|
438
|
+
}`}
|
|
439
|
+
style={{ fontFamily: 'inherit' }}
|
|
440
|
+
>
|
|
441
|
+
All
|
|
442
|
+
</button>
|
|
443
|
+
{Array.from(connectorSources.entries()).map(([cid, info]) => {
|
|
444
|
+
const active = connectorFilter === cid
|
|
445
|
+
const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
|
|
446
|
+
return (
|
|
447
|
+
<button
|
|
448
|
+
key={cid}
|
|
449
|
+
onClick={() => setConnectorFilter(active ? null : cid)}
|
|
450
|
+
className={`flex items-center gap-1.5 px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
|
|
451
|
+
active ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
|
|
452
|
+
}`}
|
|
453
|
+
style={{ fontFamily: 'inherit' }}
|
|
454
|
+
>
|
|
455
|
+
<ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
|
|
456
|
+
{info.connectorName || meta?.label || info.platform}
|
|
457
|
+
</button>
|
|
458
|
+
)
|
|
459
|
+
})}
|
|
460
|
+
</>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
|
|
363
465
|
<div
|
|
364
466
|
ref={scrollRef}
|
|
365
467
|
onScroll={updateScrollState}
|
|
366
|
-
className="h-
|
|
468
|
+
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
469
|
>
|
|
368
470
|
<div className="flex flex-col gap-6 relative">
|
|
369
471
|
{/* Chat spine — vertical line for assistant messages */}
|
|
370
472
|
<div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06] pointer-events-none" />
|
|
473
|
+
{hasMoreMessages && (
|
|
474
|
+
<div className="flex justify-center py-3">
|
|
475
|
+
<button
|
|
476
|
+
onClick={async () => {
|
|
477
|
+
const el = scrollRef.current
|
|
478
|
+
const prevHeight = el?.scrollHeight ?? 0
|
|
479
|
+
await loadMoreMessages()
|
|
480
|
+
// Preserve scroll position after prepending
|
|
481
|
+
if (el) {
|
|
482
|
+
requestAnimationFrame(() => {
|
|
483
|
+
el.scrollTop += el.scrollHeight - prevHeight
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}}
|
|
487
|
+
disabled={loadingMore}
|
|
488
|
+
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"
|
|
489
|
+
>
|
|
490
|
+
{loadingMore ? (
|
|
491
|
+
<>
|
|
492
|
+
<span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-text-2 animate-spin" />
|
|
493
|
+
Loading...
|
|
494
|
+
</>
|
|
495
|
+
) : (
|
|
496
|
+
<>
|
|
497
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
|
498
|
+
<path d="M12 19V5" />
|
|
499
|
+
<path d="m5 12 7-7 7 7" />
|
|
500
|
+
</svg>
|
|
501
|
+
Load earlier messages
|
|
502
|
+
<span className="text-text-3/50">({totalMessages - messages.length} more)</span>
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
371
508
|
{filteredMessages.length === 0 && !streaming && (
|
|
372
509
|
<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
510
|
<AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
|
|
@@ -378,6 +515,41 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
378
515
|
</div>
|
|
379
516
|
)}
|
|
380
517
|
{filteredMessages.map((msg, i) => {
|
|
518
|
+
// Context-clear divider — render a visual separator instead of a bubble
|
|
519
|
+
if (msg.kind === 'context-clear') {
|
|
520
|
+
const originalIndex = messages.indexOf(msg)
|
|
521
|
+
return (
|
|
522
|
+
<div key={`ctx-clear-${msg.time}-${i}`} className="group/ctx flex items-center gap-4 py-3">
|
|
523
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
524
|
+
<span className="flex items-center gap-1.5 text-[10px] font-600 text-amber-400/60 uppercase tracking-[0.1em]">
|
|
525
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="shrink-0">
|
|
526
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
527
|
+
<polyline points="8 8 4 12 8 16" />
|
|
528
|
+
<polyline points="16 8 20 12 16 16" />
|
|
529
|
+
</svg>
|
|
530
|
+
New context
|
|
531
|
+
{msg.time ? ` · ${new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''}
|
|
532
|
+
</span>
|
|
533
|
+
{sessionId && originalIndex >= 0 && (
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
onClick={async () => {
|
|
537
|
+
try {
|
|
538
|
+
await api('DELETE', `/sessions/${sessionId}/messages`, { messageIndex: originalIndex })
|
|
539
|
+
setMessages(messages.filter((_: Message, idx: number) => idx !== originalIndex))
|
|
540
|
+
} catch { /* best-effort */ }
|
|
541
|
+
}}
|
|
542
|
+
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"
|
|
543
|
+
title="Undo — restore full context"
|
|
544
|
+
>
|
|
545
|
+
Undo
|
|
546
|
+
</button>
|
|
547
|
+
)}
|
|
548
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
549
|
+
</div>
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
|
|
381
553
|
// Find original index in the full messages array for API calls
|
|
382
554
|
const originalIndex = messages.indexOf(msg)
|
|
383
555
|
const isLastAssistant = msg.role === 'assistant' && !streaming
|
|
@@ -407,7 +579,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
407
579
|
}
|
|
408
580
|
|
|
409
581
|
return (
|
|
410
|
-
<div key={`${msg.time}-${i}`}>
|
|
582
|
+
<div key={`${msg.time}-${i}`} data-message-index={i}>
|
|
411
583
|
{showDateSep && (
|
|
412
584
|
<div className="flex items-center gap-4 py-2 mb-2">
|
|
413
585
|
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
@@ -438,11 +610,12 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
438
610
|
<ApprovalCards agentId={agent?.id} />
|
|
439
611
|
{streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
440
612
|
{streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
441
|
-
{!streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
613
|
+
{appSettings.suggestionsEnabled !== false && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
442
614
|
<SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
|
|
443
615
|
)}
|
|
444
616
|
</div>
|
|
445
617
|
</div>
|
|
618
|
+
{showGatewayOverlay && <GatewayDisconnectOverlay />}
|
|
446
619
|
{showScrollToBottom && (
|
|
447
620
|
<button
|
|
448
621
|
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
|
)}
|