@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -14
|
@@ -9,6 +9,7 @@ import { toast } from 'sonner'
|
|
|
9
9
|
import { Skeleton } from '@/components/shared/skeleton'
|
|
10
10
|
import { EmptyState } from '@/components/shared/empty-state'
|
|
11
11
|
import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
|
|
12
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
inSidebar?: boolean
|
|
@@ -38,6 +39,8 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
38
39
|
const [sortMode, setSortMode] = useState<SortMode>('lastActive')
|
|
39
40
|
const [loaded, setLoaded] = useState(Object.keys(sessions).length > 0)
|
|
40
41
|
const [bulkMenuOpen, setBulkMenuOpen] = useState(false)
|
|
42
|
+
const [confirmClearIds, setConfirmClearIds] = useState<string[] | null>(null)
|
|
43
|
+
const [clearing, setClearing] = useState(false)
|
|
41
44
|
|
|
42
45
|
useEffect(() => {
|
|
43
46
|
if (Object.keys(sessions).length > 0 && !loaded) setLoaded(true)
|
|
@@ -116,6 +119,18 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
116
119
|
onSelect?.()
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
const handleClearFiltered = async () => {
|
|
123
|
+
if (!confirmClearIds || confirmClearIds.length === 0) return
|
|
124
|
+
setClearing(true)
|
|
125
|
+
try {
|
|
126
|
+
await clearSessions(confirmClearIds)
|
|
127
|
+
toast.success(`${confirmClearIds.length} chat${confirmClearIds.length === 1 ? '' : 's'} deleted`)
|
|
128
|
+
setConfirmClearIds(null)
|
|
129
|
+
} finally {
|
|
130
|
+
setClearing(false)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
// Truly empty — no sessions at all for this user
|
|
120
135
|
if (!allUserSessions.length) {
|
|
121
136
|
// Show skeleton cards while data is loading
|
|
@@ -178,11 +193,9 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
178
193
|
</svg>
|
|
179
194
|
</button>
|
|
180
195
|
<Dropdown open={bulkMenuOpen} onClose={() => setBulkMenuOpen(false)}>
|
|
181
|
-
<DropdownItem onClick={
|
|
196
|
+
<DropdownItem onClick={() => {
|
|
182
197
|
setBulkMenuOpen(false)
|
|
183
|
-
|
|
184
|
-
await clearSessions(filtered.map((s) => s.id))
|
|
185
|
-
toast.success(`${filtered.length} chat${filtered.length === 1 ? '' : 's'} deleted`)
|
|
198
|
+
setConfirmClearIds(filtered.map((s) => s.id))
|
|
186
199
|
}}>
|
|
187
200
|
Clear filtered chats
|
|
188
201
|
</DropdownItem>
|
|
@@ -249,6 +262,17 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
249
262
|
</p>
|
|
250
263
|
</div>
|
|
251
264
|
)}
|
|
265
|
+
<ConfirmDialog
|
|
266
|
+
open={!!confirmClearIds}
|
|
267
|
+
title="Clear Filtered Chats?"
|
|
268
|
+
message={confirmClearIds ? `Delete ${confirmClearIds.length} chat${confirmClearIds.length === 1 ? '' : 's'} from the current view?` : 'Delete filtered chats?'}
|
|
269
|
+
confirmLabel={clearing ? 'Deleting...' : 'Delete'}
|
|
270
|
+
confirmDisabled={clearing}
|
|
271
|
+
cancelDisabled={clearing}
|
|
272
|
+
danger
|
|
273
|
+
onConfirm={() => { void handleClearFiltered() }}
|
|
274
|
+
onCancel={() => { if (!clearing) setConfirmClearIds(null) }}
|
|
275
|
+
/>
|
|
252
276
|
</div>
|
|
253
277
|
)
|
|
254
278
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { toast } from 'sonner'
|
|
7
8
|
|
|
@@ -21,6 +22,7 @@ export function CheckpointTimeline({ sessionId }: Props) {
|
|
|
21
22
|
const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([])
|
|
22
23
|
const [loading, setLoading] = useState(true)
|
|
23
24
|
const [restoringId, setRestoringId] = useState<string | null>(null)
|
|
25
|
+
const [confirmRestore, setConfirmRestore] = useState<Checkpoint | null>(null)
|
|
24
26
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
25
27
|
|
|
26
28
|
const load = async () => {
|
|
@@ -40,9 +42,9 @@ export function CheckpointTimeline({ sessionId }: Props) {
|
|
|
40
42
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
43
|
}, [sessionId])
|
|
42
44
|
|
|
43
|
-
const handleRestore = async (
|
|
44
|
-
if (!
|
|
45
|
-
|
|
45
|
+
const handleRestore = async () => {
|
|
46
|
+
if (!confirmRestore) return
|
|
47
|
+
const checkpoint = confirmRestore
|
|
46
48
|
setRestoringId(checkpoint.checkpointId)
|
|
47
49
|
try {
|
|
48
50
|
await api('POST', `/chats/${sessionId}/restore`, {
|
|
@@ -52,6 +54,7 @@ export function CheckpointTimeline({ sessionId }: Props) {
|
|
|
52
54
|
toast.success('Session restored successfully')
|
|
53
55
|
await loadSessions()
|
|
54
56
|
await load()
|
|
57
|
+
setConfirmRestore(null)
|
|
55
58
|
} catch (err) {
|
|
56
59
|
toast.error('Failed to restore session')
|
|
57
60
|
console.error(err)
|
|
@@ -74,39 +77,52 @@ export function CheckpointTimeline({ sessionId }: Props) {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
return (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<div className="flex
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
<>
|
|
81
|
+
<div className="flex flex-col gap-3 p-5">
|
|
82
|
+
{checkpoints.map((cp, i) => (
|
|
83
|
+
<div
|
|
84
|
+
key={cp.checkpointId}
|
|
85
|
+
className="group relative flex flex-col gap-2 p-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04] transition-all"
|
|
86
|
+
>
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<div className="flex flex-col">
|
|
89
|
+
<span className="text-[11px] font-700 text-accent-bright uppercase tracking-wider">
|
|
90
|
+
{i === 0 ? 'Current State' : `Point ${checkpoints.length - i}`}
|
|
91
|
+
</span>
|
|
92
|
+
<span className="text-[10px] text-text-3 font-mono">
|
|
93
|
+
{new Date(cp.createdAt).toLocaleString()}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
{i > 0 && (
|
|
97
|
+
<button
|
|
98
|
+
onClick={() => setConfirmRestore(cp)}
|
|
99
|
+
disabled={!!restoringId}
|
|
100
|
+
className="px-3 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600 border-none cursor-pointer hover:brightness-110 disabled:opacity-50"
|
|
101
|
+
>
|
|
102
|
+
{restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
91
105
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
>
|
|
98
|
-
{restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
|
|
99
|
-
</button>
|
|
106
|
+
|
|
107
|
+
{cp.values && Array.isArray(cp.values.messages) && cp.values.messages.length > 0 && (
|
|
108
|
+
<div className="mt-1 p-2 rounded-[8px] bg-black/20 text-[11px] text-text-3 line-clamp-2 italic">
|
|
109
|
+
Last message: {String((cp.values.messages[cp.values.messages.length - 1] as Record<string, unknown>)?.content ?? 'Empty state')}
|
|
110
|
+
</div>
|
|
100
111
|
)}
|
|
101
112
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
<ConfirmDialog
|
|
116
|
+
open={!!confirmRestore}
|
|
117
|
+
title="Restore Session?"
|
|
118
|
+
message="Restore session to this point? This will delete all subsequent history."
|
|
119
|
+
confirmLabel={restoringId ? 'Restoring...' : 'Restore'}
|
|
120
|
+
confirmDisabled={!!restoringId}
|
|
121
|
+
cancelDisabled={!!restoringId}
|
|
122
|
+
danger
|
|
123
|
+
onConfirm={() => { void handleRestore() }}
|
|
124
|
+
onCancel={() => { if (!restoringId) setConfirmRestore(null) }}
|
|
125
|
+
/>
|
|
126
|
+
</>
|
|
111
127
|
)
|
|
112
128
|
}
|
|
@@ -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 || ''}
|