@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -17,8 +17,9 @@ import { isStructuredMarkdown } from './markdown-utils'
|
|
|
17
17
|
import { FilePathChip, FILE_PATH_RE, DIR_PATH_RE } from './file-path-chip'
|
|
18
18
|
import { TransferAgentPicker } from './transfer-agent-picker'
|
|
19
19
|
import { DelegationBanner, DelegationSourceBanner, TaskCompletionCard, parseTaskCompletion } from './delegation-banner'
|
|
20
|
-
import { ConnectorPlatformIcon,
|
|
20
|
+
import { ConnectorPlatformIcon, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
|
|
21
21
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
22
|
+
import { formatMessageTimestamp } from '@/lib/chat-display'
|
|
22
23
|
|
|
23
24
|
/** Parse delegation-source metadata prefix from system messages */
|
|
24
25
|
const DELEGATION_SOURCE_RE = /^\[delegation-source:([^:]*):([^:]*):([^\]]*)\]/
|
|
@@ -33,21 +34,16 @@ function tryParseJson(s: string): Record<string, unknown> | null {
|
|
|
33
34
|
try { return JSON.parse(s) } catch { return null }
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const d = new Date(ts)
|
|
47
|
-
const today = new Date()
|
|
48
|
-
if (d.toDateString() === today.toDateString()) return fmtTime(ts)
|
|
49
|
-
if (diff < 604_800_000) return d.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' })
|
|
50
|
-
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
37
|
+
function connectorThreadMeta(message: Message, isUser: boolean): string | null {
|
|
38
|
+
const source = message.source
|
|
39
|
+
if (!source) return null
|
|
40
|
+
const connectorName = source.connectorName?.trim() || getConnectorPlatformLabel(source.platform)
|
|
41
|
+
if (isUser) {
|
|
42
|
+
const sender = source.senderName?.trim() || source.senderId?.trim() || source.channelId?.trim()
|
|
43
|
+
return sender ? `${connectorName} · ${sender}` : connectorName
|
|
44
|
+
}
|
|
45
|
+
const recipient = source.senderName?.trim() || source.senderId?.trim() || source.channelId?.trim()
|
|
46
|
+
return recipient ? `${connectorName} · to ${recipient}` : connectorName
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
interface HeartbeatMeta {
|
|
@@ -92,11 +88,7 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
92
88
|
blocked: '#EF4444',
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
|
|
96
|
-
const match = url.match(/\/api\/uploads\/([^/?#]+)/)
|
|
97
|
-
if (!match?.[1]) return false
|
|
98
|
-
return /^(browser|screenshot)-\d+\./i.test(match[1])
|
|
99
|
-
}
|
|
91
|
+
const emptyToolEvents: NonNullable<Message['toolEvents']> = []
|
|
100
92
|
|
|
101
93
|
// AttachmentChip, parseAttachmentUrl, regex constants, and FILE_TYPE_COLORS
|
|
102
94
|
// are now imported from @/components/shared/attachment-chip
|
|
@@ -180,6 +172,15 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
180
172
|
} catch { /* ignore */ }
|
|
181
173
|
return null
|
|
182
174
|
}, [message.text, isUser])
|
|
175
|
+
|
|
176
|
+
const walletActionRequest = useMemo(() => {
|
|
177
|
+
if (isUser) return null
|
|
178
|
+
try {
|
|
179
|
+
const data = JSON.parse(message.text)
|
|
180
|
+
if (data.type === 'plugin_wallet_action_request') return data
|
|
181
|
+
} catch { /* ignore */ }
|
|
182
|
+
return null
|
|
183
|
+
}, [message.text, isUser])
|
|
183
184
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
184
185
|
const [copied, setCopied] = useState(false)
|
|
185
186
|
const [heartbeatExpanded, setHeartbeatExpanded] = useState(false)
|
|
@@ -187,43 +188,48 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
187
188
|
const [editing, setEditing] = useState(false)
|
|
188
189
|
const [editText, setEditText] = useState('')
|
|
189
190
|
const [transferPickerOpen, setTransferPickerOpen] = useState(false)
|
|
190
|
-
const toolEvents = message.toolEvents
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
191
|
+
const toolEvents = message.toolEvents ?? emptyToolEvents
|
|
192
|
+
// Separate send_file events — they render as inline attachments, not in the tool accordion
|
|
193
|
+
const nonSendFileEvents = useMemo(() => toolEvents.filter((ev) => ev.name !== 'send_file' || ev.error), [toolEvents])
|
|
194
|
+
const hasToolEvents = !isUser && nonSendFileEvents.length > 0
|
|
195
|
+
const visibleToolEvents = toolEventsExpanded ? [...nonSendFileEvents].reverse() : nonSendFileEvents.slice(-1)
|
|
194
196
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const visibleMedia = extractMedia(lastOutput)
|
|
201
|
-
const hasNamedVisibleImage = visibleMedia.images.some((url) => !isGeneratedBrowserScreenshot(url))
|
|
202
|
-
const seen = new Set<string>([
|
|
203
|
-
...visibleMedia.images,
|
|
204
|
-
...visibleMedia.videos,
|
|
205
|
-
...visibleMedia.pdfs.map((p) => p.url),
|
|
206
|
-
...visibleMedia.files.map((f) => f.url),
|
|
207
|
-
])
|
|
208
|
-
const images: string[] = []
|
|
209
|
-
const videos: string[] = []
|
|
197
|
+
// Extract ALL media from ALL tool events for inline display after the message text.
|
|
198
|
+
// Covers send_file, browser screenshots, file tool outputs — everything.
|
|
199
|
+
const allToolMedia = useMemo(() => {
|
|
200
|
+
const images: { name: string; url: string }[] = []
|
|
201
|
+
const videos: { name: string; url: string }[] = []
|
|
210
202
|
const pdfs: { name: string; url: string }[] = []
|
|
211
203
|
const files: { name: string; url: string }[] = []
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
const seen = new Set<string>()
|
|
205
|
+
|
|
206
|
+
for (const ev of toolEvents) {
|
|
207
|
+
if (ev.error || !ev.output) continue
|
|
214
208
|
const m = extractMedia(ev.output)
|
|
215
209
|
for (const url of m.images) {
|
|
216
|
-
if (
|
|
217
|
-
|
|
210
|
+
if (!seen.has(url)) { seen.add(url); images.push({ name: url.split('/').pop() || 'Image', url }) }
|
|
211
|
+
}
|
|
212
|
+
for (const url of m.videos) {
|
|
213
|
+
if (!seen.has(url)) { seen.add(url); videos.push({ name: url.split('/').pop() || 'Video', url }) }
|
|
214
|
+
}
|
|
215
|
+
for (const p of m.pdfs) {
|
|
216
|
+
if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) }
|
|
217
|
+
}
|
|
218
|
+
for (const f of m.files) {
|
|
219
|
+
// Reclassify image-extension files as images (send_file uses [label](url) not )
|
|
220
|
+
if (/\.(png|jpe?g|gif|webp|svg|avif)$/i.test(f.url)) {
|
|
221
|
+
if (!seen.has(f.url)) { seen.add(f.url); images.push(f) }
|
|
222
|
+
} else {
|
|
223
|
+
if (!seen.has(f.url)) { seen.add(f.url); files.push(f) }
|
|
224
|
+
}
|
|
218
225
|
}
|
|
219
|
-
for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
|
|
220
|
-
for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
|
|
221
|
-
for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
|
|
222
226
|
}
|
|
227
|
+
|
|
223
228
|
if (!images.length && !videos.length && !pdfs.length && !files.length) return null
|
|
224
229
|
return { images, videos, pdfs, files }
|
|
225
|
-
|
|
226
|
-
}, [message.toolEvents
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, [message.toolEvents])
|
|
232
|
+
const isStructured = !isUser && !isHeartbeat && isStructuredMarkdown(message.text)
|
|
227
233
|
|
|
228
234
|
// Collect all media URLs already rendered via tool events to avoid duplicates in markdown
|
|
229
235
|
const toolEventMediaUrls = useMemo(() => {
|
|
@@ -256,6 +262,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
256
262
|
})
|
|
257
263
|
}, [message.text])
|
|
258
264
|
|
|
265
|
+
const connectorMeta = connectorThreadMeta(message, isUser)
|
|
266
|
+
|
|
259
267
|
return (
|
|
260
268
|
<div
|
|
261
269
|
className={`group ${isUser ? 'flex flex-col items-end' : 'flex flex-col items-start relative pl-[44px]'}`}
|
|
@@ -270,37 +278,46 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
270
278
|
</div>
|
|
271
279
|
)}
|
|
272
280
|
{/* Sender label + timestamp */}
|
|
273
|
-
<div className={`flex
|
|
274
|
-
<
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
<div className={`flex flex-col gap-0.5 mb-2 px-1 ${isUser ? 'items-end' : 'items-start'}`}>
|
|
282
|
+
<div className={`flex items-center gap-2.5 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
283
|
+
<span className={`text-[12px] font-600 flex items-center gap-1.5 ${isUser ? 'text-accent-bright/70' : 'text-text-3'}`}>
|
|
284
|
+
{message.source && (
|
|
285
|
+
<ConnectorPlatformIcon platform={message.source.platform} size={12} />
|
|
286
|
+
)}
|
|
287
|
+
{isUser
|
|
288
|
+
? (message.source?.senderName
|
|
289
|
+
? `${message.source.senderName} via ${getConnectorPlatformLabel(message.source.platform)}`
|
|
290
|
+
: (currentUser ? currentUser.charAt(0).toUpperCase() + currentUser.slice(1) : 'You'))
|
|
291
|
+
: (message.source
|
|
292
|
+
? `${assistantName || 'Claude'} via ${getConnectorPlatformLabel(message.source.platform)}`
|
|
293
|
+
: (assistantName || 'Claude'))}
|
|
294
|
+
</span>
|
|
295
|
+
<span className="text-[11px] text-text-3/70 font-mono" title={message.time ? new Date(message.time).toLocaleString() : ''}>
|
|
296
|
+
{message.time ? formatMessageTimestamp(message) : ''}
|
|
297
|
+
</span>
|
|
298
|
+
</div>
|
|
299
|
+
{connectorMeta && (
|
|
300
|
+
<div className={`text-[10px] font-mono text-text-3/55 ${isUser ? 'text-right' : ''}`}>
|
|
301
|
+
{connectorMeta}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
287
304
|
</div>
|
|
288
305
|
|
|
289
306
|
{/* Tool call events (assistant messages only) */}
|
|
290
307
|
{hasToolEvents && (
|
|
291
308
|
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
292
|
-
{
|
|
309
|
+
{nonSendFileEvents.length > 1 && (
|
|
293
310
|
<button
|
|
294
311
|
type="button"
|
|
295
312
|
onClick={() => setToolEventsExpanded((v) => !v)}
|
|
296
313
|
className="self-start px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-[11px] text-text-3 border border-white/[0.06] cursor-pointer transition-colors"
|
|
297
314
|
>
|
|
298
|
-
{toolEventsExpanded ? 'Show latest only' : `Show all tool calls (${
|
|
315
|
+
{toolEventsExpanded ? 'Show latest only' : `Show all tool calls (${nonSendFileEvents.length})`}
|
|
299
316
|
</button>
|
|
300
317
|
)}
|
|
301
318
|
<div className={`${toolEventsExpanded ? 'max-h-[320px] overflow-y-auto pr-1 flex flex-col gap-2' : 'flex flex-col gap-2'}`}>
|
|
302
319
|
{visibleToolEvents.map((event, i) => {
|
|
303
|
-
const key = `${message.time}-tool-${toolEventsExpanded ? `all-${i}` : `latest-${
|
|
320
|
+
const key = `${message.time}-tool-${toolEventsExpanded ? `all-${i}` : `latest-${nonSendFileEvents.length - 1}`}`
|
|
304
321
|
|
|
305
322
|
if (event.name === 'delegate_to_agent') {
|
|
306
323
|
const inp = tryParseJson(event.input || '{}')
|
|
@@ -352,83 +369,6 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
352
369
|
</div>
|
|
353
370
|
)}
|
|
354
371
|
|
|
355
|
-
{/* Media from hidden tool calls (shown when collapsed so files are never buried) */}
|
|
356
|
-
{hiddenMedia && (
|
|
357
|
-
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
358
|
-
{hiddenMedia.images.map((src, i) => (
|
|
359
|
-
<div key={`himg-${i}`} className="relative group/img">
|
|
360
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
361
|
-
<img
|
|
362
|
-
src={src}
|
|
363
|
-
alt={`Screenshot ${i + 1}`}
|
|
364
|
-
loading="lazy"
|
|
365
|
-
className="max-w-[400px] rounded-[10px] border border-white/10 cursor-pointer hover:border-white/25 transition-all"
|
|
366
|
-
onClick={() => {
|
|
367
|
-
import('@/stores/use-chat-store').then(({ useChatStore }) =>
|
|
368
|
-
useChatStore.getState().setPreviewContent({ type: 'image', url: src, title: `Screenshot ${i + 1}` })
|
|
369
|
-
)
|
|
370
|
-
}}
|
|
371
|
-
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
372
|
-
/>
|
|
373
|
-
<a
|
|
374
|
-
href={src}
|
|
375
|
-
download
|
|
376
|
-
onClick={(e) => e.stopPropagation()}
|
|
377
|
-
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"
|
|
378
|
-
title="Download"
|
|
379
|
-
>
|
|
380
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
|
381
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
382
|
-
<polyline points="7 10 12 15 17 10" />
|
|
383
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
384
|
-
</svg>
|
|
385
|
-
</a>
|
|
386
|
-
</div>
|
|
387
|
-
))}
|
|
388
|
-
{hiddenMedia.videos.map((src, i) => (
|
|
389
|
-
<video key={`hvid-${i}`} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
390
|
-
))}
|
|
391
|
-
{hiddenMedia.pdfs.map((file, i) => (
|
|
392
|
-
<div key={`hpdf-${i}`} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
393
|
-
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
394
|
-
<a
|
|
395
|
-
href={file.url}
|
|
396
|
-
download
|
|
397
|
-
onClick={(e) => e.stopPropagation()}
|
|
398
|
-
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"
|
|
399
|
-
>
|
|
400
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
401
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
402
|
-
<polyline points="7 10 12 15 17 10" />
|
|
403
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
404
|
-
</svg>
|
|
405
|
-
{file.name}
|
|
406
|
-
</a>
|
|
407
|
-
</div>
|
|
408
|
-
))}
|
|
409
|
-
{hiddenMedia.files.map((file, i) => (
|
|
410
|
-
<a
|
|
411
|
-
key={`hfile-${i}`}
|
|
412
|
-
href={file.url}
|
|
413
|
-
download
|
|
414
|
-
onClick={(e) => e.stopPropagation()}
|
|
415
|
-
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"
|
|
416
|
-
>
|
|
417
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
418
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
419
|
-
<polyline points="14 2 14 8 20 8" />
|
|
420
|
-
</svg>
|
|
421
|
-
{file.name}
|
|
422
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="ml-auto opacity-50">
|
|
423
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
424
|
-
<polyline points="7 10 12 15 17 10" />
|
|
425
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
426
|
-
</svg>
|
|
427
|
-
</a>
|
|
428
|
-
))}
|
|
429
|
-
</div>
|
|
430
|
-
)}
|
|
431
|
-
|
|
432
372
|
{/* Thinking block (collapsible, shown for assistant messages with persisted thinking) */}
|
|
433
373
|
{!isUser && message.thinking && (
|
|
434
374
|
<div className="max-w-[85%] md:max-w-[72%] mb-2">
|
|
@@ -493,7 +433,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
493
433
|
<div className="p-3 rounded-[12px] bg-black/40 border border-white/5 flex flex-col gap-2">
|
|
494
434
|
<div className="flex justify-between items-center">
|
|
495
435
|
<span className="text-[11px] text-text-3/60 font-600 uppercase">Amount</span>
|
|
496
|
-
<span className="text-[13px] font-700 text-sky-400">{walletRequest.amountSol} SOL</span>
|
|
436
|
+
<span className="text-[13px] font-700 text-sky-400">{walletRequest.amountDisplay || `${walletRequest.amountSol} SOL`}</span>
|
|
497
437
|
</div>
|
|
498
438
|
<div className="flex flex-col gap-1">
|
|
499
439
|
<span className="text-[11px] text-text-3/60 font-600 uppercase">To Address</span>
|
|
@@ -508,7 +448,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
508
448
|
</div>
|
|
509
449
|
<div className="flex gap-2 mt-1">
|
|
510
450
|
<button
|
|
511
|
-
onClick={() => useChatStore.getState().sendMessage(`I approve this transfer of ${walletRequest.amountSol} SOL to ${walletRequest.toAddress}. Proceed with wallet_tool and set approved=true.`)}
|
|
451
|
+
onClick={() => useChatStore.getState().sendMessage(`I approve this transfer of ${walletRequest.amountDisplay || `${walletRequest.amountSol} SOL`} to ${walletRequest.toAddress}. Proceed with wallet_tool and set approved=true.`)}
|
|
512
452
|
className="px-4 py-2 rounded-[12px] bg-sky-500 text-black text-[13px] font-700 hover:bg-sky-400 transition-all active:scale-[0.98]"
|
|
513
453
|
>
|
|
514
454
|
Approve & Send
|
|
@@ -521,6 +461,52 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
521
461
|
</button>
|
|
522
462
|
</div>
|
|
523
463
|
</div>
|
|
464
|
+
) : walletActionRequest ? (
|
|
465
|
+
<div className="flex flex-col gap-3 p-4 rounded-[18px] bg-violet-500/[0.03] border border-violet-500/20 shadow-[0_0_20px_rgba(139,92,246,0.05)]">
|
|
466
|
+
<div className="flex items-center gap-2 mb-1">
|
|
467
|
+
<div className="w-5 h-5 rounded-full bg-violet-500/20 flex items-center justify-center text-violet-400">
|
|
468
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
469
|
+
<path d="M12 2v8" />
|
|
470
|
+
<path d="M8 6h8" />
|
|
471
|
+
<path d="m5 19 4-4 3 3 7-7" />
|
|
472
|
+
</svg>
|
|
473
|
+
</div>
|
|
474
|
+
<span className="text-[11px] font-700 uppercase tracking-wider text-violet-400/80">Wallet Action Request</span>
|
|
475
|
+
</div>
|
|
476
|
+
<p className="text-[13px] text-text-2/90 leading-relaxed">{walletActionRequest.message}</p>
|
|
477
|
+
<div className="p-3 rounded-[12px] bg-black/40 border border-white/5 flex flex-col gap-2">
|
|
478
|
+
<div className="flex justify-between items-center gap-3">
|
|
479
|
+
<span className="text-[11px] text-text-3/60 font-600 uppercase">Action</span>
|
|
480
|
+
<span className="text-[13px] font-700 text-violet-400">{walletActionRequest.action || 'wallet_action'}</span>
|
|
481
|
+
</div>
|
|
482
|
+
{(walletActionRequest.chain || walletActionRequest.network) && (
|
|
483
|
+
<div className="flex justify-between items-center gap-3">
|
|
484
|
+
<span className="text-[11px] text-text-3/60 font-600 uppercase">Chain</span>
|
|
485
|
+
<span className="text-[12px] text-text-2/80">{[walletActionRequest.chain, walletActionRequest.network].filter(Boolean).join(' / ')}</span>
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
{walletActionRequest.summary && (
|
|
489
|
+
<div className="flex flex-col gap-1 border-t border-white/5 pt-2">
|
|
490
|
+
<span className="text-[11px] text-text-3/60 font-600 uppercase">Summary</span>
|
|
491
|
+
<span className="text-[12px] text-text-2/80 whitespace-pre-wrap break-words">{walletActionRequest.summary}</span>
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
<div className="flex gap-2 mt-1">
|
|
496
|
+
<button
|
|
497
|
+
onClick={() => useChatStore.getState().sendMessage(`I approve this wallet action (${walletActionRequest.action || 'wallet_action'}). Proceed with wallet_tool and set approved=true.`)}
|
|
498
|
+
className="px-4 py-2 rounded-[12px] bg-violet-500 text-black text-[13px] font-700 hover:bg-violet-400 transition-all active:scale-[0.98]"
|
|
499
|
+
>
|
|
500
|
+
Approve Action
|
|
501
|
+
</button>
|
|
502
|
+
<button
|
|
503
|
+
onClick={() => useChatStore.getState().sendMessage('I do not approve this wallet action. Cancel it.')}
|
|
504
|
+
className="px-4 py-2 rounded-[12px] bg-white/[0.05] hover:bg-white/[0.1] text-text-2 text-[13px] font-600 transition-all border border-white/10"
|
|
505
|
+
>
|
|
506
|
+
Reject
|
|
507
|
+
</button>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
524
510
|
) : installRequest ? (
|
|
525
511
|
<div className="flex flex-col gap-3 p-4 rounded-[18px] bg-emerald-500/[0.03] border border-emerald-500/20 shadow-[0_0_20px_rgba(16,185,129,0.05)]">
|
|
526
512
|
<div className="flex items-center gap-2 mb-1">
|
|
@@ -806,6 +792,83 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
806
792
|
</div>
|
|
807
793
|
)}
|
|
808
794
|
|
|
795
|
+
{/* Inline media from all tool outputs — images, videos, PDFs, files */}
|
|
796
|
+
{allToolMedia && (
|
|
797
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mt-1 mb-2">
|
|
798
|
+
{allToolMedia.images.map((img, i) => (
|
|
799
|
+
<div key={`tm-img-${i}`} className="relative group/img">
|
|
800
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
801
|
+
<img
|
|
802
|
+
src={img.url}
|
|
803
|
+
alt={img.name}
|
|
804
|
+
loading="lazy"
|
|
805
|
+
className="max-w-[400px] rounded-[10px] border border-white/10 cursor-pointer hover:border-white/25 transition-all"
|
|
806
|
+
onClick={() => {
|
|
807
|
+
import('@/stores/use-chat-store').then(({ useChatStore }) =>
|
|
808
|
+
useChatStore.getState().setPreviewContent({ type: 'image', url: img.url, title: img.name })
|
|
809
|
+
)
|
|
810
|
+
}}
|
|
811
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
|
812
|
+
/>
|
|
813
|
+
<a
|
|
814
|
+
href={img.url}
|
|
815
|
+
download
|
|
816
|
+
onClick={(e) => e.stopPropagation()}
|
|
817
|
+
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"
|
|
818
|
+
title="Download"
|
|
819
|
+
>
|
|
820
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
|
821
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
822
|
+
<polyline points="7 10 12 15 17 10" />
|
|
823
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
824
|
+
</svg>
|
|
825
|
+
</a>
|
|
826
|
+
</div>
|
|
827
|
+
))}
|
|
828
|
+
{allToolMedia.videos.map((vid, i) => (
|
|
829
|
+
<video key={`tm-vid-${i}`} src={vid.url} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
830
|
+
))}
|
|
831
|
+
{allToolMedia.pdfs.map((file, i) => (
|
|
832
|
+
<div key={`tm-pdf-${i}`} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
833
|
+
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
834
|
+
<a
|
|
835
|
+
href={file.url}
|
|
836
|
+
download
|
|
837
|
+
onClick={(e) => e.stopPropagation()}
|
|
838
|
+
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"
|
|
839
|
+
>
|
|
840
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
841
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
842
|
+
<polyline points="7 10 12 15 17 10" />
|
|
843
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
844
|
+
</svg>
|
|
845
|
+
{file.name}
|
|
846
|
+
</a>
|
|
847
|
+
</div>
|
|
848
|
+
))}
|
|
849
|
+
{allToolMedia.files.map((file, i) => (
|
|
850
|
+
<a
|
|
851
|
+
key={`tm-file-${i}`}
|
|
852
|
+
href={file.url}
|
|
853
|
+
download
|
|
854
|
+
onClick={(e) => e.stopPropagation()}
|
|
855
|
+
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"
|
|
856
|
+
>
|
|
857
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
858
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
859
|
+
<polyline points="14 2 14 8 20 8" />
|
|
860
|
+
</svg>
|
|
861
|
+
{file.name}
|
|
862
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="ml-auto opacity-50">
|
|
863
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
864
|
+
<polyline points="7 10 12 15 17 10" />
|
|
865
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
866
|
+
</svg>
|
|
867
|
+
</a>
|
|
868
|
+
))}
|
|
869
|
+
</div>
|
|
870
|
+
)}
|
|
871
|
+
|
|
809
872
|
{/* Tool access request banners */}
|
|
810
873
|
{!isUser && <ToolRequestBanner
|
|
811
874
|
text={message.text || ''}
|
|
@@ -7,6 +7,7 @@ import { useChatStore } from '@/stores/use-chat-store'
|
|
|
7
7
|
import { useAppStore } from '@/stores/use-app-store'
|
|
8
8
|
import { api } from '@/lib/api-client'
|
|
9
9
|
import { shouldHidePersistedStreamingAssistantMessage } from '@/lib/chat-streaming-state'
|
|
10
|
+
import { dedupeMessagesForDisplay } from '@/lib/chat-display'
|
|
10
11
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
11
12
|
import { MessageBubble } from './message-bubble'
|
|
12
13
|
import { StreamingBubble } from './streaming-bubble'
|
|
@@ -47,6 +48,22 @@ function dateSeparator(ts: number): string {
|
|
|
47
48
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function getLatestAssistantToolMoment(messages: Message[]): { key: string; name: string; input: string } | null {
|
|
52
|
+
const last = messages[messages.length - 1]
|
|
53
|
+
if (!last || last.role !== 'assistant' || !last.toolEvents?.length) return null
|
|
54
|
+
const events = last.toolEvents
|
|
55
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
56
|
+
if (isNotableTool(events[i].name)) {
|
|
57
|
+
return {
|
|
58
|
+
key: `${last.time}-${events[i].name}-${i}`,
|
|
59
|
+
name: events[i].name,
|
|
60
|
+
input: events[i].input || '',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
interface Props {
|
|
51
68
|
messages: Message[]
|
|
52
69
|
streaming: boolean
|
|
@@ -90,7 +107,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
90
107
|
const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
|
|
91
108
|
|
|
92
109
|
// Moment overlay for last assistant message (heartbeat or tool events)
|
|
93
|
-
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
|
|
110
|
+
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; key: string; name: string; input: string }
|
|
94
111
|
const [currentMoment, setCurrentMoment] = useState<MomentType | null>(null)
|
|
95
112
|
|
|
96
113
|
const heartbeatTopic = agent?.id ? `heartbeat:agent:${agent.id}` : ''
|
|
@@ -98,23 +115,32 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
98
115
|
setCurrentMoment({ kind: 'heartbeat' })
|
|
99
116
|
})
|
|
100
117
|
|
|
101
|
-
// Detect notable tool events on latest assistant message when messages change
|
|
102
118
|
const prevToolKeyRef = useRef<string | null>(null)
|
|
119
|
+
const seededMomentSessionRef = useRef<string | null>(null)
|
|
120
|
+
|
|
103
121
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const key = `${last.time}-${events[i].name}-${i}`
|
|
110
|
-
if (key !== prevToolKeyRef.current) {
|
|
111
|
-
prevToolKeyRef.current = key
|
|
112
|
-
setCurrentMoment({ kind: 'tool', name: events[i].name, input: events[i].input || '' })
|
|
113
|
-
}
|
|
114
|
-
return
|
|
115
|
-
}
|
|
122
|
+
if (!sessionId) {
|
|
123
|
+
seededMomentSessionRef.current = null
|
|
124
|
+
prevToolKeyRef.current = null
|
|
125
|
+
setCurrentMoment(null)
|
|
126
|
+
return
|
|
116
127
|
}
|
|
117
|
-
|
|
128
|
+
|
|
129
|
+
if (seededMomentSessionRef.current === sessionId) return
|
|
130
|
+
seededMomentSessionRef.current = sessionId
|
|
131
|
+
prevToolKeyRef.current = getLatestAssistantToolMoment(messages)?.key || null
|
|
132
|
+
setCurrentMoment(null)
|
|
133
|
+
}, [messages, sessionId])
|
|
134
|
+
|
|
135
|
+
// Detect notable tool events on the latest assistant message after the session has been seeded.
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!sessionId || seededMomentSessionRef.current !== sessionId) return
|
|
138
|
+
const moment = getLatestAssistantToolMoment(messages)
|
|
139
|
+
if (!moment) return
|
|
140
|
+
if (moment.key === prevToolKeyRef.current) return
|
|
141
|
+
prevToolKeyRef.current = moment.key
|
|
142
|
+
setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
|
|
143
|
+
}, [messages, sessionId])
|
|
118
144
|
|
|
119
145
|
// Unread count tracking
|
|
120
146
|
const unreadRef = useRef(0)
|
|
@@ -189,13 +215,16 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
189
215
|
}
|
|
190
216
|
}
|
|
191
217
|
|
|
218
|
+
const dedupedDisplayedMessages = dedupeMessagesForDisplay(displayedMessages)
|
|
219
|
+
|
|
192
220
|
// Apply bookmark + connector filter
|
|
193
221
|
let filteredMessages = bookmarkFilter
|
|
194
|
-
?
|
|
195
|
-
:
|
|
222
|
+
? dedupedDisplayedMessages.filter((msg) => msg.bookmarked)
|
|
223
|
+
: dedupedDisplayedMessages
|
|
196
224
|
if (connectorFilter) {
|
|
197
225
|
filteredMessages = filteredMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
|
|
198
226
|
}
|
|
227
|
+
const hasVisiblePersistedStreamingMessage = filteredMessages.some((msg) => msg.role === 'assistant' && msg.streaming === true)
|
|
199
228
|
|
|
200
229
|
// Search matches
|
|
201
230
|
const searchMatches = useMemo(() => {
|
|
@@ -629,7 +658,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
629
658
|
} else {
|
|
630
659
|
momentOverlay = (
|
|
631
660
|
<ActivityMoment
|
|
632
|
-
key={
|
|
661
|
+
key={currentMoment.key}
|
|
633
662
|
toolName={currentMoment.name}
|
|
634
663
|
toolInput={currentMoment.input}
|
|
635
664
|
onDismiss={() => setCurrentMoment(null)}
|
|
@@ -676,7 +705,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
|
|
|
676
705
|
)
|
|
677
706
|
})}
|
|
678
707
|
<ApprovalCards agentId={agent?.id} />
|
|
679
|
-
{streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
|
|
708
|
+
{streaming && !displayText && !hasVisiblePersistedStreamingMessage && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
|
|
680
709
|
{streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
|
|
681
710
|
{appSettings.suggestionsEnabled === true && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
682
711
|
<SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
|
|
@@ -14,7 +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,
|
|
17
|
+
import { ConnectorPlatformIcon, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
|
|
18
18
|
import type { ChatroomMessage, Chatroom, Agent } from '@/types'
|
|
19
19
|
|
|
20
20
|
interface Props {
|
|
@@ -227,7 +227,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
227
227
|
<span className="text-[13px] font-600 text-text flex items-center gap-1.5">
|
|
228
228
|
{message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
|
|
229
229
|
{isUser && message.source?.senderName
|
|
230
|
-
? `${message.source.senderName} via ${
|
|
230
|
+
? `${message.source.senderName} via ${getConnectorPlatformLabel(message.source.platform)}`
|
|
231
231
|
: message.senderName}
|
|
232
232
|
</span>
|
|
233
233
|
)}
|