@swarmclawai/swarmclaw 0.6.4 → 0.6.6
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 +5 -3
- package/package.json +5 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
- package/src/app/api/chatrooms/[id]/route.ts +15 -1
- package/src/app/api/chatrooms/route.ts +15 -2
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/tasks/route.ts +24 -0
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +15 -0
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +1 -0
- package/src/components/agents/agent-chat-list.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +112 -26
- package/src/components/chat/chat-area.tsx +2 -2
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +3 -2
- package/src/components/chat/message-list.tsx +5 -4
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/home/home-view.tsx +2 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +18 -3
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-sheet.tsx +21 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/chat-execution.ts +35 -3
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +64 -11
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +80 -2
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +8 -5
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +226 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +6 -2
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +38 -0
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +8 -0
- package/src/types/index.ts +60 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadWallets, upsertWallet, loadAgents, saveAgents } from '@/lib/server/storage'
|
|
4
|
+
import { generateSolanaKeypair } from '@/lib/server/solana'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import type { AgentWallet, WalletChain } from '@/types'
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
/** Strip encryptedPrivateKey from wallet for safe API responses */
|
|
10
|
+
function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
|
|
11
|
+
return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET() {
|
|
15
|
+
const wallets = loadWallets() as Record<string, AgentWallet>
|
|
16
|
+
const safe = Object.fromEntries(
|
|
17
|
+
Object.entries(wallets).map(([id, w]) => [id, stripPrivateKey(w as unknown as Record<string, unknown>)]),
|
|
18
|
+
)
|
|
19
|
+
return NextResponse.json(safe)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(req: Request) {
|
|
23
|
+
const body = await req.json()
|
|
24
|
+
const agentId = typeof body.agentId === 'string' ? body.agentId.trim() : ''
|
|
25
|
+
if (!agentId) {
|
|
26
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const agents = loadAgents()
|
|
30
|
+
if (!agents[agentId]) {
|
|
31
|
+
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check agent doesn't already have a wallet
|
|
35
|
+
const existing = loadWallets() as Record<string, AgentWallet>
|
|
36
|
+
const hasWallet = Object.values(existing).some((w) => w.agentId === agentId)
|
|
37
|
+
if (hasWallet) {
|
|
38
|
+
return NextResponse.json({ error: 'Agent already has a wallet' }, { status: 409 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const chain: WalletChain = body.chain === 'solana' ? 'solana' : 'solana' // extensible later
|
|
42
|
+
const { publicKey, encryptedPrivateKey } = generateSolanaKeypair()
|
|
43
|
+
|
|
44
|
+
const id = genId()
|
|
45
|
+
const now = Date.now()
|
|
46
|
+
|
|
47
|
+
const wallet: AgentWallet = {
|
|
48
|
+
id,
|
|
49
|
+
agentId,
|
|
50
|
+
chain,
|
|
51
|
+
publicKey,
|
|
52
|
+
encryptedPrivateKey,
|
|
53
|
+
label: typeof body.label === 'string' ? body.label : undefined,
|
|
54
|
+
spendingLimitLamports: typeof body.spendingLimitLamports === 'number' ? body.spendingLimitLamports : 100_000_000,
|
|
55
|
+
dailyLimitLamports: typeof body.dailyLimitLamports === 'number' ? body.dailyLimitLamports : 1_000_000_000,
|
|
56
|
+
requireApproval: body.requireApproval !== false,
|
|
57
|
+
createdAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
upsertWallet(id, wallet)
|
|
62
|
+
|
|
63
|
+
// Link wallet to agent
|
|
64
|
+
const agent = agents[agentId]
|
|
65
|
+
agent.walletId = id
|
|
66
|
+
agent.updatedAt = now
|
|
67
|
+
agents[agentId] = agent
|
|
68
|
+
saveAgents(agents)
|
|
69
|
+
|
|
70
|
+
notify('wallets')
|
|
71
|
+
notify('agents')
|
|
72
|
+
|
|
73
|
+
return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
|
|
74
|
+
}
|
package/src/app/globals.css
CHANGED
|
@@ -215,6 +215,14 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
215
215
|
to { opacity: 1; transform: translateY(0); }
|
|
216
216
|
}
|
|
217
217
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
218
|
+
@keyframes pulse-glow {
|
|
219
|
+
0%, 100% { transform: scale(1); opacity: 0.45; }
|
|
220
|
+
50% { transform: scale(1.2); opacity: 0.85; }
|
|
221
|
+
}
|
|
222
|
+
@keyframes avatar-pulse {
|
|
223
|
+
0%, 100% { transform: scale(1); }
|
|
224
|
+
50% { transform: scale(1.1); }
|
|
225
|
+
}
|
|
218
226
|
@keyframes slide-in-left {
|
|
219
227
|
from { transform: translateX(-100%); }
|
|
220
228
|
to { transform: translateX(0); }
|
package/src/cli/index.js
CHANGED
|
@@ -485,6 +485,21 @@ const COMMAND_GROUPS = [
|
|
|
485
485
|
}),
|
|
486
486
|
],
|
|
487
487
|
},
|
|
488
|
+
{
|
|
489
|
+
name: 'wallets',
|
|
490
|
+
description: 'Manage agent wallets and wallet transactions',
|
|
491
|
+
commands: [
|
|
492
|
+
cmd('list', 'GET', '/wallets', 'List wallets'),
|
|
493
|
+
cmd('get', 'GET', '/wallets/:id', 'Get wallet by id'),
|
|
494
|
+
cmd('create', 'POST', '/wallets', 'Create wallet', { expectsJsonBody: true }),
|
|
495
|
+
cmd('update', 'PATCH', '/wallets/:id', 'Update wallet settings', { expectsJsonBody: true }),
|
|
496
|
+
cmd('delete', 'DELETE', '/wallets/:id', 'Delete wallet'),
|
|
497
|
+
cmd('send', 'POST', '/wallets/:id/send', 'Send funds from wallet', { expectsJsonBody: true }),
|
|
498
|
+
cmd('approve', 'POST', '/wallets/:id/approve', 'Approve or deny a pending wallet transaction', { expectsJsonBody: true }),
|
|
499
|
+
cmd('transactions', 'GET', '/wallets/:id/transactions', 'List wallet transactions'),
|
|
500
|
+
cmd('balance-history', 'GET', '/wallets/:id/balance-history', 'Get wallet balance history'),
|
|
501
|
+
],
|
|
502
|
+
},
|
|
488
503
|
{
|
|
489
504
|
name: 'upload',
|
|
490
505
|
description: 'Upload raw file/blob',
|
package/src/cli/spec.js
CHANGED
|
@@ -351,6 +351,20 @@ const COMMAND_GROUPS = {
|
|
|
351
351
|
metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
|
|
352
352
|
},
|
|
353
353
|
},
|
|
354
|
+
wallets: {
|
|
355
|
+
description: 'Agent wallet operations',
|
|
356
|
+
commands: {
|
|
357
|
+
list: { description: 'List wallets', method: 'GET', path: '/wallets' },
|
|
358
|
+
get: { description: 'Get wallet by id', method: 'GET', path: '/wallets/:id', params: ['id'] },
|
|
359
|
+
create: { description: 'Create wallet', method: 'POST', path: '/wallets' },
|
|
360
|
+
update: { description: 'Update wallet settings', method: 'PATCH', path: '/wallets/:id', params: ['id'] },
|
|
361
|
+
delete: { description: 'Delete wallet', method: 'DELETE', path: '/wallets/:id', params: ['id'] },
|
|
362
|
+
send: { description: 'Send funds from wallet', method: 'POST', path: '/wallets/:id/send', params: ['id'] },
|
|
363
|
+
approve: { description: 'Approve or deny pending wallet transaction', method: 'POST', path: '/wallets/:id/approve', params: ['id'] },
|
|
364
|
+
transactions: { description: 'List wallet transactions', method: 'GET', path: '/wallets/:id/transactions', params: ['id'] },
|
|
365
|
+
'balance-history': { description: 'Get wallet balance history', method: 'GET', path: '/wallets/:id/balance-history', params: ['id'] },
|
|
366
|
+
},
|
|
367
|
+
},
|
|
354
368
|
webhooks: {
|
|
355
369
|
description: 'Inbound webhook triggers',
|
|
356
370
|
commands: {
|
|
@@ -13,6 +13,7 @@ function sanitizeSvg(svg: string): string {
|
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
seed?: string | null
|
|
16
|
+
avatarUrl?: string | null
|
|
16
17
|
name: string
|
|
17
18
|
size?: number
|
|
18
19
|
className?: string
|
|
@@ -27,7 +28,7 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
27
28
|
|
|
28
29
|
const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
|
|
29
30
|
|
|
30
|
-
export function AgentAvatar({ seed, name, size = 32, className = '', status, heartbeatPulse }: Props) {
|
|
31
|
+
export function AgentAvatar({ seed, avatarUrl, name, size = 32, className = '', status, heartbeatPulse }: Props) {
|
|
31
32
|
const svgHtml = useMemo(() => {
|
|
32
33
|
if (!seed) return null
|
|
33
34
|
return sanitizeSvg(multiavatar(seed))
|
|
@@ -53,6 +54,19 @@ export function AgentAvatar({ seed, name, size = 32, className = '', status, hea
|
|
|
53
54
|
</svg>
|
|
54
55
|
) : null
|
|
55
56
|
|
|
57
|
+
if (avatarUrl) {
|
|
58
|
+
return (
|
|
59
|
+
<div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
|
|
60
|
+
<div className="rounded-full overflow-hidden w-full h-full">
|
|
61
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
62
|
+
<img src={avatarUrl} alt={name} className="w-full h-full object-cover" draggable={false} />
|
|
63
|
+
</div>
|
|
64
|
+
{heartEl}
|
|
65
|
+
{dot}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
if (svgHtml) {
|
|
57
71
|
return (
|
|
58
72
|
<div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
|
|
@@ -157,6 +157,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
|
|
|
157
157
|
<div className="flex items-center gap-2.5">
|
|
158
158
|
<AgentAvatar
|
|
159
159
|
seed={agent.avatarSeed}
|
|
160
|
+
avatarUrl={agent.avatarUrl}
|
|
160
161
|
name={agent.name}
|
|
161
162
|
size={28}
|
|
162
163
|
status={isRunning ? 'busy' : isOnline ? 'online' : undefined}
|
|
@@ -223,7 +223,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
223
223
|
>
|
|
224
224
|
<div className="flex items-center gap-2.5">
|
|
225
225
|
<div className="relative shrink-0">
|
|
226
|
-
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={36} />
|
|
226
|
+
<AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={36} />
|
|
227
227
|
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
|
|
228
228
|
isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/30'
|
|
229
229
|
}`} />
|
|
@@ -7,7 +7,8 @@ import { api } from '@/lib/api-client'
|
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
8
|
import { toast } from 'sonner'
|
|
9
9
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
10
|
-
import type { ProviderType, ClaudeSkill } from '@/types'
|
|
10
|
+
import type { ProviderType, ClaudeSkill, AgentWallet } from '@/types'
|
|
11
|
+
import { WalletSection } from '@/components/wallets/wallet-section'
|
|
11
12
|
import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
|
|
12
13
|
import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
13
14
|
import { AgentAvatar } from './agent-avatar'
|
|
@@ -105,12 +106,15 @@ export function AgentSheet() {
|
|
|
105
106
|
const [openclawEnabled, setOpenclawEnabled] = useState(false)
|
|
106
107
|
const [projectId, setProjectId] = useState<string | undefined>(undefined)
|
|
107
108
|
const [avatarSeed, setAvatarSeed] = useState('')
|
|
109
|
+
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
|
|
110
|
+
const [uploading, setUploading] = useState(false)
|
|
108
111
|
const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
|
|
109
112
|
const [voiceId, setVoiceId] = useState('')
|
|
110
113
|
const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
|
|
111
114
|
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
|
|
112
115
|
const [heartbeatModel, setHeartbeatModel] = useState('')
|
|
113
116
|
const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
|
|
117
|
+
const [agentWallet, setAgentWallet] = useState<(Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null>(null)
|
|
114
118
|
const [addingKey, setAddingKey] = useState(false)
|
|
115
119
|
const [newKeyName, setNewKeyName] = useState('')
|
|
116
120
|
const [newKeyValue, setNewKeyValue] = useState('')
|
|
@@ -178,18 +182,27 @@ export function AgentSheet() {
|
|
|
178
182
|
setMcpDisabledTools(editing.mcpDisabledTools || [])
|
|
179
183
|
setFallbackCredentialIds(editing.fallbackCredentialIds || [])
|
|
180
184
|
// platformAssignScope derived from isOrchestrator — no separate state
|
|
181
|
-
setCapabilities(editing.capabilities
|
|
185
|
+
setCapabilities(Array.isArray(editing.capabilities) ? editing.capabilities : [])
|
|
182
186
|
setCapInput('')
|
|
183
187
|
setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
|
|
184
188
|
setOpenclawEnabled(editing.provider === 'openclaw')
|
|
185
189
|
setProjectId(editing.projectId)
|
|
186
190
|
setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
|
|
191
|
+
setAvatarUrl(editing.avatarUrl || null)
|
|
187
192
|
setThinkingLevel(editing.thinkingLevel || '')
|
|
188
193
|
setVoiceId(editing.elevenLabsVoiceId || '')
|
|
189
194
|
setHeartbeatEnabled(editing.heartbeatEnabled || false)
|
|
190
195
|
setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
|
|
191
196
|
setHeartbeatModel(editing.heartbeatModel || '')
|
|
192
197
|
setHeartbeatPrompt(editing.heartbeatPrompt || '')
|
|
198
|
+
// Load wallet if agent has one
|
|
199
|
+
if (editing.walletId) {
|
|
200
|
+
api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${editing.walletId}`)
|
|
201
|
+
.then(setAgentWallet)
|
|
202
|
+
.catch(() => setAgentWallet(null))
|
|
203
|
+
} else {
|
|
204
|
+
setAgentWallet(null)
|
|
205
|
+
}
|
|
193
206
|
} else {
|
|
194
207
|
setName('')
|
|
195
208
|
setDescription('')
|
|
@@ -310,6 +323,7 @@ export function AgentSheet() {
|
|
|
310
323
|
capabilities,
|
|
311
324
|
projectId: projectId || undefined,
|
|
312
325
|
avatarSeed: avatarSeed.trim() || undefined,
|
|
326
|
+
avatarUrl: avatarUrl || null,
|
|
313
327
|
thinkingLevel: thinkingLevel || undefined,
|
|
314
328
|
elevenLabsVoiceId: voiceId.trim() || null,
|
|
315
329
|
heartbeatEnabled,
|
|
@@ -480,30 +494,82 @@ export function AgentSheet() {
|
|
|
480
494
|
|
|
481
495
|
<div className="mb-8">
|
|
482
496
|
<SectionLabel>Avatar</SectionLabel>
|
|
483
|
-
<div className="flex
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
497
|
+
<div className="flex flex-col gap-3">
|
|
498
|
+
<div className="flex items-center gap-4">
|
|
499
|
+
<div className="relative group shrink-0">
|
|
500
|
+
<AgentAvatar seed={avatarUrl ? null : (avatarSeed || null)} avatarUrl={avatarUrl} name={name || 'A'} size={64} />
|
|
501
|
+
<label className="absolute inset-0 rounded-full flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
|
|
502
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
503
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
|
504
|
+
<circle cx="12" cy="13" r="4" />
|
|
505
|
+
</svg>
|
|
506
|
+
<input
|
|
507
|
+
type="file"
|
|
508
|
+
accept="image/*"
|
|
509
|
+
className="hidden"
|
|
510
|
+
onChange={async (e) => {
|
|
511
|
+
const file = e.target.files?.[0]
|
|
512
|
+
if (!file) return
|
|
513
|
+
setUploading(true)
|
|
514
|
+
try {
|
|
515
|
+
const res = await fetch('/api/upload', {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers: { 'x-filename': file.name },
|
|
518
|
+
body: await file.arrayBuffer(),
|
|
519
|
+
})
|
|
520
|
+
const data = await res.json()
|
|
521
|
+
if (data.url) {
|
|
522
|
+
setAvatarUrl(data.url)
|
|
523
|
+
setAvatarSeed('')
|
|
524
|
+
}
|
|
525
|
+
} finally {
|
|
526
|
+
setUploading(false)
|
|
527
|
+
e.target.value = ''
|
|
528
|
+
}
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
531
|
+
</label>
|
|
532
|
+
</div>
|
|
533
|
+
<div className="flex flex-col gap-1.5 flex-1 min-w-0">
|
|
534
|
+
{avatarUrl && (
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
onClick={() => {
|
|
538
|
+
setAvatarUrl(null)
|
|
539
|
+
if (!avatarSeed) setAvatarSeed(crypto.randomUUID().slice(0, 8))
|
|
540
|
+
}}
|
|
541
|
+
className="text-[11px] text-text-3 hover:text-red-400 transition-colors self-start cursor-pointer"
|
|
542
|
+
>
|
|
543
|
+
Remove custom image
|
|
544
|
+
</button>
|
|
545
|
+
)}
|
|
546
|
+
{uploading && <span className="text-[11px] text-text-3">Uploading...</span>}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div className="flex items-center gap-3">
|
|
550
|
+
<input
|
|
551
|
+
type="text"
|
|
552
|
+
value={avatarSeed}
|
|
553
|
+
onChange={(e) => { setAvatarSeed(e.target.value); setAvatarUrl(null) }}
|
|
554
|
+
placeholder="Avatar seed (any text)"
|
|
555
|
+
className={inputClass}
|
|
556
|
+
style={{ fontFamily: 'inherit', flex: 1 }}
|
|
557
|
+
/>
|
|
558
|
+
<button
|
|
559
|
+
type="button"
|
|
560
|
+
onClick={() => { setAvatarSeed(crypto.randomUUID().slice(0, 8)); setAvatarUrl(null) }}
|
|
561
|
+
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] hover:text-text-2 active:scale-95 shrink-0"
|
|
562
|
+
style={{ fontFamily: 'inherit' }}
|
|
563
|
+
title="Shuffle avatar"
|
|
564
|
+
>
|
|
565
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
566
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
567
|
+
<circle cx="9" cy="9" r="1" fill="currentColor" />
|
|
568
|
+
<circle cx="15" cy="15" r="1" fill="currentColor" />
|
|
569
|
+
</svg>
|
|
570
|
+
Shuffle
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
507
573
|
</div>
|
|
508
574
|
</div>
|
|
509
575
|
|
|
@@ -666,6 +732,26 @@ export function AgentSheet() {
|
|
|
666
732
|
<p className="text-[11px] text-text-3/70 mt-1.5">Periodic check-in runs on idle sessions using this agent. Processes pending events and monitors status.</p>
|
|
667
733
|
</div>
|
|
668
734
|
|
|
735
|
+
{/* Wallet Section */}
|
|
736
|
+
{editingId && (
|
|
737
|
+
<WalletSection
|
|
738
|
+
agentId={editingId}
|
|
739
|
+
wallet={agentWallet}
|
|
740
|
+
onWalletCreated={async () => {
|
|
741
|
+
await loadAgents()
|
|
742
|
+
// Fetch the wallet for this agent
|
|
743
|
+
try {
|
|
744
|
+
const wallets = await api<Record<string, Omit<AgentWallet, 'encryptedPrivateKey'>>>('GET', '/wallets')
|
|
745
|
+
const match = Object.values(wallets).find((w) => w.agentId === editingId)
|
|
746
|
+
if (match) {
|
|
747
|
+
const detail = await api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${match.id}`)
|
|
748
|
+
setAgentWallet(detail)
|
|
749
|
+
}
|
|
750
|
+
} catch { /* ignore */ }
|
|
751
|
+
}}
|
|
752
|
+
/>
|
|
753
|
+
)}
|
|
754
|
+
|
|
669
755
|
{provider !== 'openclaw' && (
|
|
670
756
|
<div className="mb-8">
|
|
671
757
|
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -146,8 +146,8 @@ export function ChatArea() {
|
|
|
146
146
|
if (!sessionId) return
|
|
147
147
|
try {
|
|
148
148
|
const msgs = await fetchMessages(sessionId)
|
|
149
|
-
if (msgs.length
|
|
150
|
-
const newMsgs = msgs.slice(messagesLenRef.current)
|
|
149
|
+
if (msgs.length !== messagesLenRef.current) {
|
|
150
|
+
const newMsgs = msgs.length > messagesLenRef.current ? msgs.slice(messagesLenRef.current) : []
|
|
151
151
|
setMessages(msgs)
|
|
152
152
|
if (ttsEnabledRef.current && typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
|
153
153
|
const latestAssistant = [...newMsgs].reverse().find((m) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useMemo, useRef } from 'react'
|
|
3
|
+
import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
|
|
4
4
|
import type { Session } from '@/types'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -17,6 +17,7 @@ import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
|
17
17
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
18
18
|
import { toast } from 'sonner'
|
|
19
19
|
import type { ProviderType } from '@/types'
|
|
20
|
+
import { useWs } from '@/hooks/use-ws'
|
|
20
21
|
|
|
21
22
|
function shortPath(p: string): string {
|
|
22
23
|
return (p || '').replace(/^\/Users\/\w+/, '~')
|
|
@@ -106,6 +107,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
106
107
|
const [renameError, setRenameError] = useState('')
|
|
107
108
|
const renameInputRef = useRef<HTMLInputElement>(null)
|
|
108
109
|
const renameContainerRef = useRef<HTMLSpanElement>(null)
|
|
110
|
+
const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
|
|
111
|
+
const [walletBalance, setWalletBalance] = useState<number | null>(null)
|
|
112
|
+
|
|
113
|
+
const fetchWalletBalance = useCallback(async () => {
|
|
114
|
+
if (!agent?.walletId) { setWalletBalance(null); return }
|
|
115
|
+
try {
|
|
116
|
+
const data = await api<{ balanceSol?: number }>('GET', `/wallets/${agent.walletId}`)
|
|
117
|
+
setWalletBalance(data.balanceSol ?? null)
|
|
118
|
+
} catch { setWalletBalance(null) }
|
|
119
|
+
}, [agent?.walletId])
|
|
120
|
+
|
|
121
|
+
useEffect(() => { fetchWalletBalance() }, [fetchWalletBalance])
|
|
122
|
+
useWs('wallets', fetchWalletBalance)
|
|
109
123
|
|
|
110
124
|
// Find linked task for this session
|
|
111
125
|
const linkedTask = useMemo(() => {
|
|
@@ -458,7 +472,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
458
472
|
const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
|
|
459
473
|
const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
|
|
460
474
|
const hasSourceFilter = !!hasMultipleSources
|
|
461
|
-
const hasContextBar = !!(
|
|
475
|
+
const hasContextBar = !!(isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
|
|
462
476
|
|
|
463
477
|
return (
|
|
464
478
|
<header
|
|
@@ -486,26 +500,26 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
486
500
|
<div className="relative shrink-0">
|
|
487
501
|
{streaming && (
|
|
488
502
|
<div
|
|
489
|
-
className="absolute -inset-[
|
|
503
|
+
className="absolute -inset-[4px] rounded-full"
|
|
490
504
|
style={{
|
|
491
|
-
background: '
|
|
492
|
-
animation: '
|
|
493
|
-
filter: 'blur(
|
|
505
|
+
background: 'radial-gradient(circle, var(--color-accent-bright), transparent 70%)',
|
|
506
|
+
animation: 'pulse-glow 2s ease-in-out infinite',
|
|
507
|
+
filter: 'blur(5px)',
|
|
494
508
|
}}
|
|
495
509
|
/>
|
|
496
510
|
)}
|
|
497
511
|
<div
|
|
498
|
-
className="relative rounded-full"
|
|
512
|
+
className="relative rounded-full transition-transform duration-500"
|
|
499
513
|
style={{
|
|
500
514
|
padding: 2,
|
|
501
515
|
background: streaming
|
|
502
|
-
? '
|
|
516
|
+
? 'linear-gradient(135deg, var(--color-accent-bright), var(--color-accent))'
|
|
503
517
|
: 'linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03))',
|
|
504
|
-
animation: streaming ? '
|
|
518
|
+
animation: streaming ? 'avatar-pulse 2s ease-in-out infinite' : undefined,
|
|
505
519
|
}}
|
|
506
520
|
>
|
|
507
521
|
<div className="rounded-full bg-bg">
|
|
508
|
-
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={hasContextBar ? 44 : 34} />
|
|
522
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={(hasContextBar || hasToolToggles) ? 44 : 34} />
|
|
509
523
|
</div>
|
|
510
524
|
</div>
|
|
511
525
|
</div>
|
|
@@ -513,8 +527,9 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
513
527
|
|
|
514
528
|
{/* Identity + metadata — fills center */}
|
|
515
529
|
<div className="flex-1 min-w-0 flex items-center gap-3">
|
|
516
|
-
{/* Name +
|
|
517
|
-
<div className="flex
|
|
530
|
+
{/* Name (row 1) + tools (row 2) */}
|
|
531
|
+
<div className="flex flex-col gap-0.5 min-w-0 shrink">
|
|
532
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
518
533
|
{renaming && agent ? (
|
|
519
534
|
<span ref={renameContainerRef} className="inline-flex items-center gap-2">
|
|
520
535
|
<input
|
|
@@ -585,10 +600,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
585
600
|
<span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
586
601
|
)}
|
|
587
602
|
</div>
|
|
603
|
+
{hasToolToggles && <ChatToolToggles session={session} />}
|
|
604
|
+
</div>
|
|
588
605
|
|
|
589
|
-
{/* Metadata tray:
|
|
606
|
+
{/* Metadata tray: wallet · model · path · status */}
|
|
590
607
|
<div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
|
|
591
608
|
<span className="text-text-3/10 text-[10px] select-none shrink-0">/</span>
|
|
609
|
+
{walletBalance !== null && (
|
|
610
|
+
<>
|
|
611
|
+
<button
|
|
612
|
+
type="button"
|
|
613
|
+
onClick={() => { setWalletPanelAgentId(agent!.id); setActiveView('wallets') }}
|
|
614
|
+
className="inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-[11px] text-text-3/45 font-mono hover:text-text-3/70 hover:bg-white/[0.04] transition-colors"
|
|
615
|
+
title="View wallet"
|
|
616
|
+
>
|
|
617
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
618
|
+
<rect x="2" y="6" width="20" height="14" rx="2" /><path d="M22 10H18a2 2 0 0 0 0 4h4" />
|
|
619
|
+
</svg>
|
|
620
|
+
{walletBalance.toFixed(3)} SOL
|
|
621
|
+
</button>
|
|
622
|
+
<span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
|
|
623
|
+
</>
|
|
624
|
+
)}
|
|
592
625
|
{modelName && (
|
|
593
626
|
<div className="relative shrink-0" ref={modelSwitcherRef}>
|
|
594
627
|
<button
|
|
@@ -727,7 +760,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
727
760
|
</button>
|
|
728
761
|
{hbDropdownOpen && (
|
|
729
762
|
<div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
|
|
730
|
-
{[1800, 3600, 7200, 21600, 43200].map((sec) => (
|
|
763
|
+
{[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
|
|
731
764
|
<button
|
|
732
765
|
key={sec}
|
|
733
766
|
onClick={() => handleSelectHeartbeatInterval(sec)}
|
|
@@ -809,11 +842,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
809
842
|
|
|
810
843
|
{/* Context bar: tools, mission controls, links */}
|
|
811
844
|
{hasContextBar && (
|
|
812
|
-
<div className="flex items-center gap-1.5 px-3.5 pb-1.5
|
|
813
|
-
{hasToolToggles && <ChatToolToggles session={session} />}
|
|
814
|
-
{hasToolToggles && (hasMemoryLink || isMainSession || linkedTask || resumeHandle || isOpenClawAgent || browserActive) && (
|
|
815
|
-
<div className="w-px h-4 bg-white/[0.05] shrink-0" />
|
|
816
|
-
)}
|
|
845
|
+
<div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
|
|
817
846
|
{isMainSession && (
|
|
818
847
|
<>
|
|
819
848
|
<button
|
|
@@ -76,7 +76,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
76
76
|
{group.tools.map((tool) => {
|
|
77
77
|
const enabled = sessionTools.includes(tool.id)
|
|
78
78
|
return (
|
|
79
|
-
<label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer">
|
|
79
|
+
<label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer" title={tool.description}>
|
|
80
80
|
<div
|
|
81
81
|
onClick={() => toggleTool(tool.id)}
|
|
82
82
|
className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { parseTaskCompletion } from './delegation-banner'
|
|
4
|
+
|
|
5
|
+
describe('parseTaskCompletion', () => {
|
|
6
|
+
it('extracts output files and report path from completion payload', () => {
|
|
7
|
+
const text = [
|
|
8
|
+
'Task completed: **[Build docs](#task:abc12345)**',
|
|
9
|
+
'',
|
|
10
|
+
'Working directory: `/tmp/work`',
|
|
11
|
+
'',
|
|
12
|
+
'Output files:',
|
|
13
|
+
'- `docs/guide.md`',
|
|
14
|
+
'- `docs/faq.md`',
|
|
15
|
+
'',
|
|
16
|
+
'Task report: `data/task-reports/abc12345.md`',
|
|
17
|
+
'',
|
|
18
|
+
'Done.',
|
|
19
|
+
].join('\n')
|
|
20
|
+
const parsed = parseTaskCompletion(text)
|
|
21
|
+
assert.ok(parsed)
|
|
22
|
+
assert.deepEqual(parsed?.outputFiles, ['docs/guide.md', 'docs/faq.md'])
|
|
23
|
+
assert.equal(parsed?.reportPath, 'data/task-reports/abc12345.md')
|
|
24
|
+
assert.equal(parsed?.workingDir, '/tmp/work')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|