@swarmclawai/swarmclaw 0.5.3 → 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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -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/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- 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/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -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 +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- 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 +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- 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/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- 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 +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- 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/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { memo, useState, useCallback,
|
|
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,129 +9,26 @@ 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
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}>({ running: false, loading: false })
|
|
29
|
-
|
|
30
|
-
// Check if a server is already running for this path on mount
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!canServe) return
|
|
33
|
-
api<{ running: boolean; url?: string; type?: string }>('POST', '/preview-server', { action: 'status', path: filePath })
|
|
34
|
-
.then((res) => { if (res.running) setServerState({ running: true, url: res.url, type: res.type, loading: false }) })
|
|
35
|
-
.catch((err) => console.error('Dev server check failed:', err))
|
|
36
|
-
}, [filePath, canServe])
|
|
37
|
-
|
|
38
|
-
const handleStartServer = async () => {
|
|
39
|
-
setServerState((s) => ({ ...s, loading: true }))
|
|
40
|
-
try {
|
|
41
|
-
const res = await api<{ running: boolean; url?: string; type?: string; framework?: string }>('POST', '/preview-server', { action: 'start', path: filePath })
|
|
42
|
-
setServerState({ running: res.running, url: res.url, type: res.type, framework: res.framework, loading: false })
|
|
43
|
-
} catch {
|
|
44
|
-
setServerState((s) => ({ ...s, loading: false }))
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const handleStopServer = async () => {
|
|
49
|
-
setServerState((s) => ({ ...s, loading: true }))
|
|
50
|
-
try {
|
|
51
|
-
await api('POST', '/preview-server', { action: 'stop', path: filePath })
|
|
52
|
-
setServerState({ running: false, loading: false })
|
|
53
|
-
} catch {
|
|
54
|
-
setServerState((s) => ({ ...s, loading: false }))
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const frameworkLabel = serverState.framework
|
|
59
|
-
? serverState.framework.charAt(0).toUpperCase() + serverState.framework.slice(1)
|
|
60
|
-
: null
|
|
14
|
+
import { AttachmentChip, parseAttachmentUrl } from '@/components/shared/attachment-chip'
|
|
15
|
+
import { isStructuredMarkdown } from './markdown-utils'
|
|
16
|
+
import { FilePathChip, FILE_PATH_RE, DIR_PATH_RE } from './file-path-chip'
|
|
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
|
+
}
|
|
61
28
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
66
|
-
<polyline points="14 2 14 8 20 8" />
|
|
67
|
-
</svg>
|
|
68
|
-
<span className="text-sky-400">{filePath}</span>
|
|
69
|
-
{canPreview && !serverState.running && (
|
|
70
|
-
<a
|
|
71
|
-
href={serveUrl}
|
|
72
|
-
target="_blank"
|
|
73
|
-
rel="noopener noreferrer"
|
|
74
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-white/[0.06] hover:bg-white/[0.10] text-[10px] font-600 text-text-3 hover:text-text-2 no-underline transition-colors cursor-pointer"
|
|
75
|
-
title="Open file"
|
|
76
|
-
>
|
|
77
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
78
|
-
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
79
|
-
<polyline points="15 3 21 3 21 9" />
|
|
80
|
-
<line x1="10" y1="14" x2="21" y2="3" />
|
|
81
|
-
</svg>
|
|
82
|
-
Open
|
|
83
|
-
</a>
|
|
84
|
-
)}
|
|
85
|
-
{canServe && !serverState.running && (
|
|
86
|
-
<button
|
|
87
|
-
onClick={handleStartServer}
|
|
88
|
-
disabled={serverState.loading}
|
|
89
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-400 text-[10px] font-600 border-none cursor-pointer transition-colors disabled:opacity-50"
|
|
90
|
-
title="Start preview server — auto-detects npm projects (React, Next, Vite, etc.) and runs the dev command"
|
|
91
|
-
>
|
|
92
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
|
|
93
|
-
<polygon points="5 3 19 12 5 21 5 3" />
|
|
94
|
-
</svg>
|
|
95
|
-
{serverState.loading ? 'Starting...' : 'Serve'}
|
|
96
|
-
</button>
|
|
97
|
-
)}
|
|
98
|
-
{canServe && serverState.running && (
|
|
99
|
-
<>
|
|
100
|
-
{frameworkLabel && (
|
|
101
|
-
<span className="px-1.5 py-0.5 rounded-[4px] bg-indigo-500/15 text-indigo-300 text-[9px] font-700 uppercase tracking-wider">
|
|
102
|
-
{frameworkLabel}
|
|
103
|
-
</span>
|
|
104
|
-
)}
|
|
105
|
-
{serverState.type === 'npm' && (
|
|
106
|
-
<span className="px-1.5 py-0.5 rounded-[4px] bg-amber-500/15 text-amber-300 text-[9px] font-700 uppercase tracking-wider">
|
|
107
|
-
npm
|
|
108
|
-
</span>
|
|
109
|
-
)}
|
|
110
|
-
<a
|
|
111
|
-
href={serverState.url}
|
|
112
|
-
target="_blank"
|
|
113
|
-
rel="noopener noreferrer"
|
|
114
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-400 text-[10px] font-600 no-underline transition-colors"
|
|
115
|
-
title="Open preview server"
|
|
116
|
-
>
|
|
117
|
-
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" style={{ animation: 'pulse 2s ease infinite' }} />
|
|
118
|
-
{serverState.url}
|
|
119
|
-
</a>
|
|
120
|
-
<button
|
|
121
|
-
onClick={handleStopServer}
|
|
122
|
-
disabled={serverState.loading}
|
|
123
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[10px] font-600 border-none cursor-pointer transition-colors disabled:opacity-50"
|
|
124
|
-
title="Stop preview server"
|
|
125
|
-
>
|
|
126
|
-
<svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor">
|
|
127
|
-
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
128
|
-
</svg>
|
|
129
|
-
Stop
|
|
130
|
-
</button>
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
</span>
|
|
134
|
-
)
|
|
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 }
|
|
135
32
|
}
|
|
136
33
|
|
|
137
34
|
function fmtTime(ts: number): string {
|
|
@@ -151,9 +48,26 @@ function relativeTime(ts: number): string {
|
|
|
151
48
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
152
49
|
}
|
|
153
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
|
+
|
|
154
67
|
function heartbeatSummary(text: string): string {
|
|
155
68
|
const clean = (text || '')
|
|
156
69
|
.replace(/\bHEARTBEAT_OK\b/gi, '')
|
|
70
|
+
.replace(/\[AGENT_HEARTBEAT_META\]\s*\{[^\n]*\}/gi, '')
|
|
157
71
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
158
72
|
.replace(/\*(.*?)\*/g, '$1')
|
|
159
73
|
.replace(/`([^`]+)`/g, '$1')
|
|
@@ -169,166 +83,15 @@ function heartbeatSummary(text: string): string {
|
|
|
169
83
|
return clean.length > 180 ? `${clean.slice(0, 180)}...` : clean
|
|
170
84
|
}
|
|
171
85
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
html: 'text-orange-400', htm: 'text-orange-400', svg: 'text-emerald-400',
|
|
178
|
-
js: 'text-yellow-400', jsx: 'text-yellow-400', ts: 'text-blue-400', tsx: 'text-blue-400',
|
|
179
|
-
py: 'text-green-400', json: 'text-amber-300', css: 'text-purple-400', scss: 'text-pink-400',
|
|
180
|
-
md: 'text-text-2', txt: 'text-text-3', pdf: 'text-red-400',
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function parseAttachmentUrl(filePath?: string, fileUrl?: string) {
|
|
184
|
-
const url = fileUrl || (filePath ? `/api/uploads/${filePath.split('/').pop()}` : '')
|
|
185
|
-
const rawName = filePath?.split('/').pop() || fileUrl?.split('/').pop() || 'file'
|
|
186
|
-
const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
|
|
187
|
-
return { url, filename }
|
|
86
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
87
|
+
progress: '#F59E0B',
|
|
88
|
+
ok: '#22C55E',
|
|
89
|
+
idle: '#6B7280',
|
|
90
|
+
blocked: '#EF4444',
|
|
188
91
|
}
|
|
189
92
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const isCode = CODE_ATTACH_RE.test(filename)
|
|
193
|
-
const isPdf = PDF_ATTACH_RE.test(filename)
|
|
194
|
-
const [lightbox, setLightbox] = useState(false)
|
|
195
|
-
const [codePreview, setCodePreview] = useState<string | null>(null)
|
|
196
|
-
const [codeExpanded, setCodeExpanded] = useState(false)
|
|
197
|
-
|
|
198
|
-
if (isImage) {
|
|
199
|
-
return (
|
|
200
|
-
<>
|
|
201
|
-
<img
|
|
202
|
-
src={url} alt="Attached"
|
|
203
|
-
loading="lazy"
|
|
204
|
-
className="max-w-[240px] rounded-[12px] mb-2 border border-white/10 cursor-pointer hover:border-white/25 transition-colors"
|
|
205
|
-
onClick={() => setLightbox(true)}
|
|
206
|
-
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
207
|
-
/>
|
|
208
|
-
{lightbox && (
|
|
209
|
-
<div
|
|
210
|
-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer"
|
|
211
|
-
onClick={() => setLightbox(false)}
|
|
212
|
-
>
|
|
213
|
-
<img src={url} alt="Preview" className="max-w-[90vw] max-h-[90vh] rounded-[12px] shadow-2xl" />
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
</>
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (isPdf) {
|
|
221
|
-
return (
|
|
222
|
-
<div className="mb-2 rounded-[12px] border border-white/[0.08] bg-[rgba(255,255,255,0.02)] overflow-hidden" style={{ maxWidth: 480 }}>
|
|
223
|
-
<div className="flex items-center gap-3 px-4 py-2.5">
|
|
224
|
-
<div className="flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 bg-red-500/10 text-red-400">
|
|
225
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
226
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
227
|
-
<polyline points="14 2 14 8 20 8" />
|
|
228
|
-
</svg>
|
|
229
|
-
</div>
|
|
230
|
-
<span className="text-[13px] font-500 truncate flex-1">{filename}</span>
|
|
231
|
-
<a href={url} download={filename} className="text-[11px] font-600 text-text-3 hover:text-text-2 no-underline">Download</a>
|
|
232
|
-
</div>
|
|
233
|
-
<iframe src={url} loading="lazy" className="w-full h-[300px] border-t border-white/[0.06]" title={filename} />
|
|
234
|
-
</div>
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
|
239
|
-
const colorClass = FILE_TYPE_COLORS[ext] || 'text-text-3'
|
|
240
|
-
const isPreviewable = PREVIEWABLE_ATTACH_RE.test(filename)
|
|
241
|
-
|
|
242
|
-
const chipBg = isUserMsg
|
|
243
|
-
? 'bg-[rgba(0,0,0,0.25)] border-white/[0.12]'
|
|
244
|
-
: 'bg-[rgba(255,255,255,0.04)] border-white/[0.08]'
|
|
245
|
-
const iconBg = isUserMsg ? 'bg-white/[0.12]' : 'bg-white/[0.05]'
|
|
246
|
-
const btnBg = isUserMsg
|
|
247
|
-
? 'bg-white/[0.12] hover:bg-white/[0.18] text-white/80'
|
|
248
|
-
: 'bg-white/[0.06] hover:bg-white/[0.10] text-text-3'
|
|
249
|
-
|
|
250
|
-
const handleCodePreview = async () => {
|
|
251
|
-
if (codePreview !== null) { setCodeExpanded(!codeExpanded); return }
|
|
252
|
-
try {
|
|
253
|
-
const serveUrl = `/api/files/serve?path=${encodeURIComponent(url.replace('/api/uploads/', ''))}`
|
|
254
|
-
const res = await fetch(url.startsWith('/api/files/') ? url : serveUrl)
|
|
255
|
-
if (!res.ok) return
|
|
256
|
-
const text = await res.text()
|
|
257
|
-
setCodePreview(text)
|
|
258
|
-
setCodeExpanded(true)
|
|
259
|
-
} catch {
|
|
260
|
-
// ignore
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return (
|
|
265
|
-
<div className="mb-2">
|
|
266
|
-
<div className={`flex items-center gap-3 px-4 py-2.5 rounded-[12px] border ${chipBg}`}>
|
|
267
|
-
<div className={`flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 ${iconBg} ${colorClass}`}>
|
|
268
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
269
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
270
|
-
<polyline points="14 2 14 8 20 8" />
|
|
271
|
-
</svg>
|
|
272
|
-
</div>
|
|
273
|
-
<div className="flex flex-col flex-1 min-w-0">
|
|
274
|
-
<span className={`text-[13px] font-500 truncate ${isUserMsg ? 'text-white' : 'text-text'}`}>{filename}</span>
|
|
275
|
-
<span className={`text-[11px] uppercase tracking-wide ${isUserMsg ? 'text-white/50' : 'text-text-3/70'}`}>{ext || 'file'}</span>
|
|
276
|
-
</div>
|
|
277
|
-
{isCode && (
|
|
278
|
-
<button
|
|
279
|
-
onClick={handleCodePreview}
|
|
280
|
-
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 border-none cursor-pointer ${
|
|
281
|
-
isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
|
|
282
|
-
}`}
|
|
283
|
-
>
|
|
284
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
285
|
-
<polyline points="16 18 22 12 16 6" />
|
|
286
|
-
<polyline points="8 6 2 12 8 18" />
|
|
287
|
-
</svg>
|
|
288
|
-
{codeExpanded ? 'Hide' : 'Preview'}
|
|
289
|
-
</button>
|
|
290
|
-
)}
|
|
291
|
-
{isPreviewable && (
|
|
292
|
-
<a href={url} target="_blank" rel="noopener noreferrer"
|
|
293
|
-
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${
|
|
294
|
-
isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
|
|
295
|
-
}`}
|
|
296
|
-
title="Preview in new tab">
|
|
297
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
298
|
-
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
299
|
-
<circle cx="12" cy="12" r="3" />
|
|
300
|
-
</svg>
|
|
301
|
-
Preview
|
|
302
|
-
</a>
|
|
303
|
-
)}
|
|
304
|
-
<a href={url} download={filename}
|
|
305
|
-
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${btnBg}`}>
|
|
306
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
307
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
308
|
-
<polyline points="7 10 12 15 17 10" />
|
|
309
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
310
|
-
</svg>
|
|
311
|
-
Download
|
|
312
|
-
</a>
|
|
313
|
-
</div>
|
|
314
|
-
{isCode && codeExpanded && codePreview !== null && (
|
|
315
|
-
<div className="mt-1 rounded-[10px] border border-white/[0.06] overflow-hidden" style={{ animation: 'fade-in 0.2s ease' }}>
|
|
316
|
-
<CodeBlock className={`language-${ext}`}>
|
|
317
|
-
{codePreview.split('\n').slice(0, codeExpanded ? undefined : 10).join('\n')}
|
|
318
|
-
</CodeBlock>
|
|
319
|
-
{codePreview.split('\n').length > 10 && (
|
|
320
|
-
<button
|
|
321
|
-
onClick={() => setCodeExpanded((v) => !v)}
|
|
322
|
-
className="w-full px-3 py-1.5 text-[10px] text-text-3 hover:text-text-2 bg-white/[0.02] hover:bg-white/[0.04] border-none border-t border-white/[0.06] cursor-pointer transition-colors"
|
|
323
|
-
>
|
|
324
|
-
{codePreview.split('\n').length > 10 ? `Show all ${codePreview.split('\n').length} lines` : 'Show less'}
|
|
325
|
-
</button>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
329
|
-
</div>
|
|
330
|
-
)
|
|
331
|
-
}
|
|
93
|
+
// AttachmentChip, parseAttachmentUrl, regex constants, and FILE_TYPE_COLORS
|
|
94
|
+
// are now imported from @/components/shared/attachment-chip
|
|
332
95
|
|
|
333
96
|
function renderAttachments(message: Message) {
|
|
334
97
|
const isUser = message.role === 'user'
|
|
@@ -374,18 +137,11 @@ interface Props {
|
|
|
374
137
|
onToggleBookmark?: (index: number) => void
|
|
375
138
|
onEditResend?: (index: number, newText: string) => void
|
|
376
139
|
onFork?: (index: number) => void
|
|
140
|
+
onTransferToAgent?: (messageIndex: number, agentId: string) => void
|
|
141
|
+
momentOverlay?: React.ReactNode
|
|
377
142
|
}
|
|
378
143
|
|
|
379
|
-
function
|
|
380
|
-
if (!text) return false
|
|
381
|
-
return /```/.test(text)
|
|
382
|
-
|| /^#{1,4}\s/m.test(text)
|
|
383
|
-
|| /^[-*]\s/m.test(text)
|
|
384
|
-
|| /^\d+\.\s/m.test(text)
|
|
385
|
-
|| /\|.*\|.*\|/m.test(text)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork }: Props) {
|
|
144
|
+
export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
|
|
389
145
|
const isUser = message.role === 'user'
|
|
390
146
|
const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
|
|
391
147
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
@@ -394,11 +150,61 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
394
150
|
const [toolEventsExpanded, setToolEventsExpanded] = useState(false)
|
|
395
151
|
const [editing, setEditing] = useState(false)
|
|
396
152
|
const [editText, setEditText] = useState('')
|
|
153
|
+
const [transferPickerOpen, setTransferPickerOpen] = useState(false)
|
|
397
154
|
const toolEvents = message.toolEvents || []
|
|
398
155
|
const hasToolEvents = !isUser && toolEvents.length > 0
|
|
399
156
|
const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
|
|
400
157
|
const isStructured = !isUser && !isHeartbeat && isStructuredMarkdown(message.text)
|
|
401
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
|
+
|
|
402
208
|
const handleCopy = useCallback(() => {
|
|
403
209
|
navigator.clipboard.writeText(message.text).then(() => {
|
|
404
210
|
setCopied(true)
|
|
@@ -408,14 +214,29 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
408
214
|
|
|
409
215
|
return (
|
|
410
216
|
<div
|
|
411
|
-
className={`group ${isUser ? 'flex flex-col items-end' : 'flex flex-col items-start'}`}
|
|
217
|
+
className={`group ${isUser ? 'flex flex-col items-end' : 'flex flex-col items-start relative pl-[44px]'}`}
|
|
412
218
|
style={{ animation: `${isUser ? 'msg-in-right' : 'msg-in-left'} 0.35s cubic-bezier(0.16, 1, 0.3, 1)` }}
|
|
413
219
|
>
|
|
220
|
+
{/* Avatar on spine (assistant) */}
|
|
221
|
+
{!isUser && (
|
|
222
|
+
<div className="absolute left-[4px] top-0">
|
|
223
|
+
<div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
|
|
224
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" />}
|
|
225
|
+
</div>
|
|
226
|
+
{momentOverlay}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
414
229
|
{/* Sender label + timestamp */}
|
|
415
230
|
<div className={`flex items-center gap-2.5 mb-2 px-1 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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')}
|
|
419
240
|
</span>
|
|
420
241
|
<span className="text-[11px] text-text-3/70 font-mono" title={message.time ? new Date(message.time).toLocaleString() : ''}>
|
|
421
242
|
{message.time ? relativeTime(message.time) : ''}
|
|
@@ -435,23 +256,183 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
435
256
|
</button>
|
|
436
257
|
)}
|
|
437
258
|
<div className={`${toolEventsExpanded ? 'max-h-[320px] overflow-y-auto pr-1 flex flex-col gap-2' : 'flex flex-col gap-2'}`}>
|
|
438
|
-
{visibleToolEvents.map((event, i) =>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
)
|
|
447
327
|
}}
|
|
328
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
448
329
|
/>
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
))}
|
|
451
386
|
</div>
|
|
452
387
|
)}
|
|
453
388
|
|
|
454
|
-
{/*
|
|
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>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
|
|
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 */
|
|
455
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'}`}>
|
|
456
437
|
{renderAttachments(message)}
|
|
457
438
|
|
|
@@ -464,12 +445,43 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
464
445
|
>
|
|
465
446
|
<div className="flex items-center justify-between gap-3">
|
|
466
447
|
<div className="flex items-center gap-2">
|
|
467
|
-
|
|
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
|
+
})()}
|
|
468
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
|
+
})()}
|
|
469
460
|
</div>
|
|
470
461
|
<span className="text-[11px] text-text-3">{heartbeatExpanded ? 'Collapse' : 'Expand'}</span>
|
|
471
462
|
</div>
|
|
472
|
-
|
|
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
|
+
})()}
|
|
473
485
|
</button>
|
|
474
486
|
{heartbeatExpanded && (
|
|
475
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">
|
|
@@ -487,7 +499,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
487
499
|
},
|
|
488
500
|
}}
|
|
489
501
|
>
|
|
490
|
-
{message.text}
|
|
502
|
+
{message.text.replace(/\[AGENT_HEARTBEAT_META\]\s*\{[^\n]*\}/gi, '').trim()}
|
|
491
503
|
</ReactMarkdown>
|
|
492
504
|
</div>
|
|
493
505
|
)}
|
|
@@ -515,6 +527,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
515
527
|
},
|
|
516
528
|
img({ src, alt }) {
|
|
517
529
|
if (!src || typeof src !== 'string') return null
|
|
530
|
+
// Skip images already rendered via tool events
|
|
531
|
+
if (toolEventMediaUrls?.has(src)) return null
|
|
518
532
|
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(src)
|
|
519
533
|
if (isVideo) {
|
|
520
534
|
return (
|
|
@@ -538,6 +552,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
538
552
|
onClick={async () => {
|
|
539
553
|
const store = useAppStore.getState()
|
|
540
554
|
await store.loadTasks(true)
|
|
555
|
+
store.setTaskSheetViewOnly(true)
|
|
541
556
|
store.setEditingTaskId(taskMatch[1])
|
|
542
557
|
store.setTaskSheetOpen(true)
|
|
543
558
|
}}
|
|
@@ -610,11 +625,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
610
625
|
},
|
|
611
626
|
}}
|
|
612
627
|
>
|
|
613
|
-
{
|
|
628
|
+
{displayText}
|
|
614
629
|
</ReactMarkdown>
|
|
615
630
|
</div>
|
|
616
631
|
)}
|
|
617
632
|
</div>
|
|
633
|
+
)}
|
|
618
634
|
|
|
619
635
|
{/* Tool access request banners */}
|
|
620
636
|
{!isUser && <ToolRequestBanner
|
|
@@ -625,10 +641,10 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
625
641
|
{/* Bookmark indicator */}
|
|
626
642
|
{message.bookmarked && (
|
|
627
643
|
<div className={`flex items-center gap-1 mt-1 px-1 ${isUser ? 'justify-end' : ''}`}>
|
|
628
|
-
<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">
|
|
629
645
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
630
646
|
</svg>
|
|
631
|
-
<span className="text-[10px] text-
|
|
647
|
+
<span className="text-[10px] text-amber-400/70 font-600">Bookmarked</span>
|
|
632
648
|
</div>
|
|
633
649
|
)}
|
|
634
650
|
|
|
@@ -651,9 +667,9 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
651
667
|
<button
|
|
652
668
|
onClick={() => onToggleBookmark(messageIndex)}
|
|
653
669
|
aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
|
|
654
|
-
className=
|
|
655
|
-
text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all
|
|
656
|
-
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' }}
|
|
657
673
|
>
|
|
658
674
|
<svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
659
675
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
@@ -709,6 +725,31 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
709
725
|
Retry
|
|
710
726
|
</button>
|
|
711
727
|
)}
|
|
728
|
+
{!isUser && typeof messageIndex === 'number' && onTransferToAgent && (
|
|
729
|
+
<div className="relative">
|
|
730
|
+
<button
|
|
731
|
+
onClick={() => setTransferPickerOpen(!transferPickerOpen)}
|
|
732
|
+
aria-label="Transfer to another agent"
|
|
733
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
734
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
735
|
+
style={{ fontFamily: 'inherit' }}
|
|
736
|
+
>
|
|
737
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
738
|
+
<path d="M8 3L4 7l4 4" />
|
|
739
|
+
<path d="M4 7h16" />
|
|
740
|
+
<path d="M16 21l4-4-4-4" />
|
|
741
|
+
<path d="M20 17H4" />
|
|
742
|
+
</svg>
|
|
743
|
+
Transfer
|
|
744
|
+
</button>
|
|
745
|
+
{transferPickerOpen && (
|
|
746
|
+
<TransferAgentPicker
|
|
747
|
+
onSelect={(agentId) => { onTransferToAgent(messageIndex, agentId); setTransferPickerOpen(false) }}
|
|
748
|
+
onClose={() => setTransferPickerOpen(false)}
|
|
749
|
+
/>
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
712
753
|
</div>
|
|
713
754
|
|
|
714
755
|
{/* Inline edit mode */}
|