@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { memo, useState, useCallback } from 'react'
|
|
3
|
+
import { memo, useState, useCallback, useMemo } from 'react'
|
|
4
4
|
import ReactMarkdown from 'react-markdown'
|
|
5
5
|
import remarkGfm from 'remark-gfm'
|
|
6
6
|
import rehypeHighlight from 'rehype-highlight'
|
|
@@ -9,12 +9,27 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
9
9
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
10
10
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
11
11
|
import { CodeBlock } from './code-block'
|
|
12
|
-
import { ToolCallBubble } from './tool-call-bubble'
|
|
12
|
+
import { ToolCallBubble, extractMedia } from './tool-call-bubble'
|
|
13
13
|
import { ToolRequestBanner } from './tool-request-banner'
|
|
14
14
|
import { AttachmentChip, parseAttachmentUrl } from '@/components/shared/attachment-chip'
|
|
15
15
|
import { isStructuredMarkdown } from './markdown-utils'
|
|
16
16
|
import { FilePathChip, FILE_PATH_RE, DIR_PATH_RE } from './file-path-chip'
|
|
17
17
|
import { TransferAgentPicker } from './transfer-agent-picker'
|
|
18
|
+
import { DelegationBanner, DelegationSourceBanner, TaskCompletionCard, parseTaskCompletion } from './delegation-banner'
|
|
19
|
+
import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
|
|
20
|
+
|
|
21
|
+
/** Parse delegation-source metadata prefix from system messages */
|
|
22
|
+
const DELEGATION_SOURCE_RE = /^\[delegation-source:([^:]*):([^:]*):([^\]]*)\]/
|
|
23
|
+
function parseDelegationSource(text: string): { delegatorId: string; delegatorName: string; delegatorAvatarSeed: string; rest: string } | null {
|
|
24
|
+
const m = text.match(DELEGATION_SOURCE_RE)
|
|
25
|
+
if (!m) return null
|
|
26
|
+
return { delegatorId: m[1], delegatorName: m[2], delegatorAvatarSeed: m[3], rest: text.slice(m[0].length).replace(/^\n/, '') }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Try to parse JSON safely, returning null on failure */
|
|
30
|
+
function tryParseJson(s: string): Record<string, unknown> | null {
|
|
31
|
+
try { return JSON.parse(s) } catch { return null }
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
function fmtTime(ts: number): string {
|
|
20
35
|
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
@@ -33,9 +48,26 @@ function relativeTime(ts: number): string {
|
|
|
33
48
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
interface HeartbeatMeta {
|
|
52
|
+
goal?: string
|
|
53
|
+
status?: string
|
|
54
|
+
next_action?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseHeartbeatMeta(text: string): HeartbeatMeta | null {
|
|
58
|
+
const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
|
|
59
|
+
if (!match?.[1]) return null
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(match[1])
|
|
62
|
+
if (typeof parsed === 'object' && parsed !== null) return parsed as HeartbeatMeta
|
|
63
|
+
} catch { /* ignore */ }
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
function heartbeatSummary(text: string): string {
|
|
37
68
|
const clean = (text || '')
|
|
38
69
|
.replace(/\bHEARTBEAT_OK\b/gi, '')
|
|
70
|
+
.replace(/\[AGENT_HEARTBEAT_META\]\s*\{[^\n]*\}/gi, '')
|
|
39
71
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
40
72
|
.replace(/\*(.*?)\*/g, '$1')
|
|
41
73
|
.replace(/`([^`]+)`/g, '$1')
|
|
@@ -51,6 +83,13 @@ function heartbeatSummary(text: string): string {
|
|
|
51
83
|
return clean.length > 180 ? `${clean.slice(0, 180)}...` : clean
|
|
52
84
|
}
|
|
53
85
|
|
|
86
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
87
|
+
progress: '#F59E0B',
|
|
88
|
+
ok: '#22C55E',
|
|
89
|
+
idle: '#6B7280',
|
|
90
|
+
blocked: '#EF4444',
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
// AttachmentChip, parseAttachmentUrl, regex constants, and FILE_TYPE_COLORS
|
|
55
94
|
// are now imported from @/components/shared/attachment-chip
|
|
56
95
|
|
|
@@ -117,6 +156,55 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
117
156
|
const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
|
|
118
157
|
const isStructured = !isUser && !isHeartbeat && isStructuredMarkdown(message.text)
|
|
119
158
|
|
|
159
|
+
// When collapsed, collect media from hidden tool events so files are always visible
|
|
160
|
+
const hiddenMedia = useMemo(() => {
|
|
161
|
+
if (toolEventsExpanded || toolEvents.length <= 1) return null
|
|
162
|
+
// Collect URLs from the visible (last) tool event to avoid showing duplicates
|
|
163
|
+
const lastOutput = toolEvents[toolEvents.length - 1]?.output || ''
|
|
164
|
+
const visibleMedia = extractMedia(lastOutput)
|
|
165
|
+
const seen = new Set<string>([
|
|
166
|
+
...visibleMedia.images,
|
|
167
|
+
...visibleMedia.videos,
|
|
168
|
+
...visibleMedia.pdfs.map((p) => p.url),
|
|
169
|
+
...visibleMedia.files.map((f) => f.url),
|
|
170
|
+
])
|
|
171
|
+
const images: string[] = []
|
|
172
|
+
const videos: string[] = []
|
|
173
|
+
const pdfs: { name: string; url: string }[] = []
|
|
174
|
+
const files: { name: string; url: string }[] = []
|
|
175
|
+
for (const ev of toolEvents.slice(0, -1)) {
|
|
176
|
+
if (!ev.output) continue
|
|
177
|
+
const m = extractMedia(ev.output)
|
|
178
|
+
for (const url of m.images) { if (!seen.has(url)) { seen.add(url); images.push(url) } }
|
|
179
|
+
for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
|
|
180
|
+
for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
|
|
181
|
+
for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
|
|
182
|
+
}
|
|
183
|
+
if (!images.length && !videos.length && !pdfs.length && !files.length) return null
|
|
184
|
+
return { images, videos, pdfs, files }
|
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
+
}, [message.toolEvents, toolEventsExpanded])
|
|
187
|
+
|
|
188
|
+
// Collect all media URLs already rendered via tool events to avoid duplicates in markdown
|
|
189
|
+
const toolEventMediaUrls = useMemo(() => {
|
|
190
|
+
if (!toolEvents.length) return null
|
|
191
|
+
const urls = new Set<string>()
|
|
192
|
+
for (const ev of toolEvents) {
|
|
193
|
+
if (!ev.output) continue
|
|
194
|
+
const m = extractMedia(ev.output)
|
|
195
|
+
for (const url of m.images) urls.add(url)
|
|
196
|
+
for (const url of m.videos) urls.add(url)
|
|
197
|
+
}
|
|
198
|
+
return urls.size > 0 ? urls : null
|
|
199
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
200
|
+
}, [message.toolEvents])
|
|
201
|
+
|
|
202
|
+
// Detect delegation-source system messages
|
|
203
|
+
const delegationSource = !isUser && message.kind === 'system' ? parseDelegationSource(message.text || '') : null
|
|
204
|
+
// Detect task completion system messages (delegated or direct)
|
|
205
|
+
const taskCompletion = !isUser && message.kind === 'system' ? parseTaskCompletion(message.text || '') : null
|
|
206
|
+
const displayText = delegationSource ? delegationSource.rest : message.text
|
|
207
|
+
|
|
120
208
|
const handleCopy = useCallback(() => {
|
|
121
209
|
navigator.clipboard.writeText(message.text).then(() => {
|
|
122
210
|
setCopied(true)
|
|
@@ -140,8 +228,15 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
140
228
|
)}
|
|
141
229
|
{/* Sender label + timestamp */}
|
|
142
230
|
<div className={`flex items-center gap-2.5 mb-2 px-1 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
143
|
-
<span className={`text-[12px] font-600 ${isUser ? 'text-accent-bright/70' : 'text-text-3'}`}>
|
|
144
|
-
{
|
|
231
|
+
<span className={`text-[12px] font-600 flex items-center gap-1.5 ${isUser ? 'text-accent-bright/70' : 'text-text-3'}`}>
|
|
232
|
+
{message.source && (
|
|
233
|
+
<ConnectorPlatformIcon platform={message.source.platform} size={12} />
|
|
234
|
+
)}
|
|
235
|
+
{isUser
|
|
236
|
+
? (message.source?.senderName
|
|
237
|
+
? `${message.source.senderName} via ${CONNECTOR_PLATFORM_META[message.source.platform]?.label || message.source.platform}`
|
|
238
|
+
: (currentUser ? currentUser.charAt(0).toUpperCase() + currentUser.slice(1) : 'You'))
|
|
239
|
+
: (assistantName || 'Claude')}
|
|
145
240
|
</span>
|
|
146
241
|
<span className="text-[11px] text-text-3/70 font-mono" title={message.time ? new Date(message.time).toLocaleString() : ''}>
|
|
147
242
|
{message.time ? relativeTime(message.time) : ''}
|
|
@@ -161,23 +256,183 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
161
256
|
</button>
|
|
162
257
|
)}
|
|
163
258
|
<div className={`${toolEventsExpanded ? 'max-h-[320px] overflow-y-auto pr-1 flex flex-col gap-2' : 'flex flex-col gap-2'}`}>
|
|
164
|
-
{visibleToolEvents.map((event, i) =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
259
|
+
{visibleToolEvents.map((event, i) => {
|
|
260
|
+
const key = `${message.time}-tool-${toolEventsExpanded ? `all-${i}` : `latest-${toolEvents.length - 1}`}`
|
|
261
|
+
|
|
262
|
+
if (event.name === 'delegate_to_agent') {
|
|
263
|
+
const inp = tryParseJson(event.input || '{}')
|
|
264
|
+
const out = tryParseJson(event.output || '{}')
|
|
265
|
+
return (
|
|
266
|
+
<DelegationBanner
|
|
267
|
+
key={key}
|
|
268
|
+
agentName={out?.agentName as string || inp?.agentName as string || inp?.agentId as string || 'Agent'}
|
|
269
|
+
agentAvatarSeed={(out?.agentAvatarSeed as string) || null}
|
|
270
|
+
taskPreview={(inp?.task as string || '').slice(0, 100)}
|
|
271
|
+
taskId={(out?.taskId as string) || null}
|
|
272
|
+
status="delegating"
|
|
273
|
+
/>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (event.name === 'check_delegation_status') {
|
|
278
|
+
const out = tryParseJson(event.output || '{}')
|
|
279
|
+
const rawStatus = out?.status as string || ''
|
|
280
|
+
const mapped = rawStatus === 'completed' ? 'completed' as const
|
|
281
|
+
: rawStatus === 'failed' ? 'failed' as const
|
|
282
|
+
: 'checking' as const
|
|
283
|
+
return (
|
|
284
|
+
<DelegationBanner
|
|
285
|
+
key={key}
|
|
286
|
+
agentName={out?.agentName as string || 'Agent'}
|
|
287
|
+
agentAvatarSeed={(out?.agentAvatarSeed as string) || null}
|
|
288
|
+
taskPreview={(out?.title as string || '').slice(0, 100)}
|
|
289
|
+
taskId={(out?.taskId as string) || null}
|
|
290
|
+
status={mapped}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<ToolCallBubble
|
|
297
|
+
key={key}
|
|
298
|
+
event={{
|
|
299
|
+
id: `${message.time}-${toolEventsExpanded ? i : toolEvents.length - 1}`,
|
|
300
|
+
name: event.name,
|
|
301
|
+
input: event.input,
|
|
302
|
+
output: event.output,
|
|
303
|
+
status: event.error ? 'error' : 'done',
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
)
|
|
307
|
+
})}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{/* Media from hidden tool calls (shown when collapsed so files are never buried) */}
|
|
313
|
+
{hiddenMedia && (
|
|
314
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
315
|
+
{hiddenMedia.images.map((src, i) => (
|
|
316
|
+
<div key={`himg-${i}`} className="relative group/img">
|
|
317
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
318
|
+
<img
|
|
319
|
+
src={src}
|
|
320
|
+
alt={`Screenshot ${i + 1}`}
|
|
321
|
+
loading="lazy"
|
|
322
|
+
className="max-w-[400px] rounded-[10px] border border-white/10 cursor-pointer hover:border-white/25 transition-all"
|
|
323
|
+
onClick={() => {
|
|
324
|
+
import('@/stores/use-chat-store').then(({ useChatStore }) =>
|
|
325
|
+
useChatStore.getState().setPreviewContent({ type: 'image', url: src, title: `Screenshot ${i + 1}` })
|
|
326
|
+
)
|
|
173
327
|
}}
|
|
328
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
174
329
|
/>
|
|
175
|
-
|
|
176
|
-
|
|
330
|
+
<a
|
|
331
|
+
href={src}
|
|
332
|
+
download
|
|
333
|
+
onClick={(e) => e.stopPropagation()}
|
|
334
|
+
className="absolute top-2 right-2 bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80 opacity-0 group-hover/img:opacity-100 transition-opacity"
|
|
335
|
+
title="Download"
|
|
336
|
+
>
|
|
337
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
|
338
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
339
|
+
<polyline points="7 10 12 15 17 10" />
|
|
340
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
341
|
+
</svg>
|
|
342
|
+
</a>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
{hiddenMedia.videos.map((src, i) => (
|
|
346
|
+
<video key={`hvid-${i}`} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
347
|
+
))}
|
|
348
|
+
{hiddenMedia.pdfs.map((file, i) => (
|
|
349
|
+
<div key={`hpdf-${i}`} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
350
|
+
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
351
|
+
<a
|
|
352
|
+
href={file.url}
|
|
353
|
+
download
|
|
354
|
+
onClick={(e) => e.stopPropagation()}
|
|
355
|
+
className="flex items-center gap-2 px-3 py-2 bg-surface/80 border-t border-white/10 text-[12px] text-text-2 hover:text-text no-underline transition-colors"
|
|
356
|
+
>
|
|
357
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
358
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
359
|
+
<polyline points="7 10 12 15 17 10" />
|
|
360
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
361
|
+
</svg>
|
|
362
|
+
{file.name}
|
|
363
|
+
</a>
|
|
364
|
+
</div>
|
|
365
|
+
))}
|
|
366
|
+
{hiddenMedia.files.map((file, i) => (
|
|
367
|
+
<a
|
|
368
|
+
key={`hfile-${i}`}
|
|
369
|
+
href={file.url}
|
|
370
|
+
download
|
|
371
|
+
onClick={(e) => e.stopPropagation()}
|
|
372
|
+
className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/10 bg-surface/60 hover:bg-surface-2 transition-colors text-[13px] text-text-2 hover:text-text no-underline"
|
|
373
|
+
>
|
|
374
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
375
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
376
|
+
<polyline points="14 2 14 8 20 8" />
|
|
377
|
+
</svg>
|
|
378
|
+
{file.name}
|
|
379
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="ml-auto opacity-50">
|
|
380
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
381
|
+
<polyline points="7 10 12 15 17 10" />
|
|
382
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
383
|
+
</svg>
|
|
384
|
+
</a>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Thinking block (collapsible, shown for assistant messages with persisted thinking) */}
|
|
390
|
+
{!isUser && message.thinking && (
|
|
391
|
+
<div className="max-w-[85%] md:max-w-[72%] mb-2">
|
|
392
|
+
<details className="group rounded-[12px] border border-purple-500/15 bg-purple-500/[0.04]">
|
|
393
|
+
<summary className="flex items-center gap-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
|
|
394
|
+
<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">
|
|
395
|
+
<polyline points="9 18 15 12 9 6" />
|
|
396
|
+
</svg>
|
|
397
|
+
<span className="text-[11px] font-600 text-purple-400/70 uppercase tracking-[0.05em]">Thinking</span>
|
|
398
|
+
<span className="text-[10px] text-text-3/40 font-mono">{Math.ceil(message.thinking.length / 4)} tokens</span>
|
|
399
|
+
</summary>
|
|
400
|
+
<div className="px-3.5 pb-3 pt-1 max-h-[300px] overflow-y-auto">
|
|
401
|
+
<div className="text-[13px] leading-[1.6] text-text-3/70 whitespace-pre-wrap break-words">
|
|
402
|
+
{message.thinking}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</details>
|
|
177
406
|
</div>
|
|
178
407
|
)}
|
|
179
408
|
|
|
180
|
-
{/*
|
|
409
|
+
{/* Delegation source banner (receiving agent's chat) */}
|
|
410
|
+
{delegationSource && (() => {
|
|
411
|
+
const taskLinkMatch = delegationSource.rest.match(/\[([^\]]+)\]\(#task:([^)]+)\)/)
|
|
412
|
+
const dsTaskTitle = taskLinkMatch?.[1] || ''
|
|
413
|
+
const dsTaskId = taskLinkMatch?.[2] || null
|
|
414
|
+
const descLines = delegationSource.rest.split('\n\n').slice(1).filter((l) => !l.startsWith('Working directory:') && !l.startsWith("I'll begin"))
|
|
415
|
+
const dsDescription = descLines.join(' ').trim().slice(0, 200)
|
|
416
|
+
return (
|
|
417
|
+
<div className="max-w-[85%] md:max-w-[72%] mb-2">
|
|
418
|
+
<DelegationSourceBanner
|
|
419
|
+
delegatorName={delegationSource.delegatorName}
|
|
420
|
+
delegatorAvatarSeed={delegationSource.delegatorAvatarSeed || null}
|
|
421
|
+
taskTitle={dsTaskTitle}
|
|
422
|
+
taskId={dsTaskId}
|
|
423
|
+
description={dsDescription}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
)
|
|
427
|
+
})()}
|
|
428
|
+
|
|
429
|
+
{/* Task completion card (replaces bubble for task result system messages) */}
|
|
430
|
+
{taskCompletion ? (
|
|
431
|
+
<div className="max-w-[85%] md:max-w-[72%]">
|
|
432
|
+
<TaskCompletionCard info={{ ...taskCompletion, imageUrl: message.imageUrl }} />
|
|
433
|
+
</div>
|
|
434
|
+
) : (
|
|
435
|
+
/* Message bubble */
|
|
181
436
|
<div className={`${isStructured ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
|
|
182
437
|
{renderAttachments(message)}
|
|
183
438
|
|
|
@@ -190,12 +445,43 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
190
445
|
>
|
|
191
446
|
<div className="flex items-center justify-between gap-3">
|
|
192
447
|
<div className="flex items-center gap-2">
|
|
193
|
-
|
|
448
|
+
{(() => {
|
|
449
|
+
const meta = parseHeartbeatMeta(message.text)
|
|
450
|
+
const statusColor = meta?.status ? (STATUS_COLORS[meta.status] || '#6B7280') : '#22C55E'
|
|
451
|
+
return <span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: statusColor }} />
|
|
452
|
+
})()}
|
|
194
453
|
<span className="text-[11px] uppercase tracking-[0.08em] text-text-2 font-600">Heartbeat</span>
|
|
454
|
+
{(() => {
|
|
455
|
+
const meta = parseHeartbeatMeta(message.text)
|
|
456
|
+
if (!meta?.status) return null
|
|
457
|
+
const color = STATUS_COLORS[meta.status] || '#6B7280'
|
|
458
|
+
return <span className="text-[10px] font-500 px-1.5 py-0.5 rounded-[4px]" style={{ color, background: `${color}18` }}>{meta.status}</span>
|
|
459
|
+
})()}
|
|
195
460
|
</div>
|
|
196
461
|
<span className="text-[11px] text-text-3">{heartbeatExpanded ? 'Collapse' : 'Expand'}</span>
|
|
197
462
|
</div>
|
|
198
|
-
|
|
463
|
+
{(() => {
|
|
464
|
+
const meta = parseHeartbeatMeta(message.text)
|
|
465
|
+
if (meta && (meta.goal || meta.next_action)) {
|
|
466
|
+
return (
|
|
467
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
468
|
+
{meta.goal && (
|
|
469
|
+
<div className="flex items-baseline gap-1.5">
|
|
470
|
+
<span className="text-[10px] uppercase tracking-[0.06em] text-text-3 font-600 shrink-0">Goal</span>
|
|
471
|
+
<span className="text-[12px] text-text-2/90 truncate">{meta.goal}</span>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
{meta.next_action && (
|
|
475
|
+
<div className="flex items-baseline gap-1.5">
|
|
476
|
+
<span className="text-[10px] uppercase tracking-[0.06em] text-text-3 font-600 shrink-0">Next</span>
|
|
477
|
+
<span className="text-[12px] text-text-2/90 truncate">{meta.next_action}</span>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
</div>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
return <p className="text-[13px] text-text-2/90 leading-[1.5] mt-1.5">{heartbeatSummary(message.text)}</p>
|
|
484
|
+
})()}
|
|
199
485
|
</button>
|
|
200
486
|
{heartbeatExpanded && (
|
|
201
487
|
<div className="msg-content text-[14px] leading-[1.7] text-text break-words px-3 py-2 rounded-[10px] border border-white/[0.08] bg-black/20">
|
|
@@ -213,7 +499,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
213
499
|
},
|
|
214
500
|
}}
|
|
215
501
|
>
|
|
216
|
-
{message.text}
|
|
502
|
+
{message.text.replace(/\[AGENT_HEARTBEAT_META\]\s*\{[^\n]*\}/gi, '').trim()}
|
|
217
503
|
</ReactMarkdown>
|
|
218
504
|
</div>
|
|
219
505
|
)}
|
|
@@ -241,6 +527,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
241
527
|
},
|
|
242
528
|
img({ src, alt }) {
|
|
243
529
|
if (!src || typeof src !== 'string') return null
|
|
530
|
+
// Skip images already rendered via tool events
|
|
531
|
+
if (toolEventMediaUrls?.has(src)) return null
|
|
244
532
|
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(src)
|
|
245
533
|
if (isVideo) {
|
|
246
534
|
return (
|
|
@@ -264,6 +552,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
264
552
|
onClick={async () => {
|
|
265
553
|
const store = useAppStore.getState()
|
|
266
554
|
await store.loadTasks(true)
|
|
555
|
+
store.setTaskSheetViewOnly(true)
|
|
267
556
|
store.setEditingTaskId(taskMatch[1])
|
|
268
557
|
store.setTaskSheetOpen(true)
|
|
269
558
|
}}
|
|
@@ -336,11 +625,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
336
625
|
},
|
|
337
626
|
}}
|
|
338
627
|
>
|
|
339
|
-
{
|
|
628
|
+
{displayText}
|
|
340
629
|
</ReactMarkdown>
|
|
341
630
|
</div>
|
|
342
631
|
)}
|
|
343
632
|
</div>
|
|
633
|
+
)}
|
|
344
634
|
|
|
345
635
|
{/* Tool access request banners */}
|
|
346
636
|
{!isUser && <ToolRequestBanner
|
|
@@ -351,10 +641,10 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
351
641
|
{/* Bookmark indicator */}
|
|
352
642
|
{message.bookmarked && (
|
|
353
643
|
<div className={`flex items-center gap-1 mt-1 px-1 ${isUser ? 'justify-end' : ''}`}>
|
|
354
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="
|
|
644
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" className="shrink-0 text-amber-400">
|
|
355
645
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
356
646
|
</svg>
|
|
357
|
-
<span className="text-[10px] text-
|
|
647
|
+
<span className="text-[10px] text-amber-400/70 font-600">Bookmarked</span>
|
|
358
648
|
</div>
|
|
359
649
|
)}
|
|
360
650
|
|
|
@@ -377,9 +667,9 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
377
667
|
<button
|
|
378
668
|
onClick={() => onToggleBookmark(messageIndex)}
|
|
379
669
|
aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
|
|
380
|
-
className=
|
|
381
|
-
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all
|
|
382
|
-
style={{ fontFamily: 'inherit'
|
|
670
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
671
|
+
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all ${message.bookmarked ? 'text-amber-400' : ''}`}
|
|
672
|
+
style={{ fontFamily: 'inherit' }}
|
|
383
673
|
>
|
|
384
674
|
<svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
385
675
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|