@swarmclawai/swarmclaw 0.6.3 → 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/app/page.tsx +7 -3
- 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/auth/access-key-gate.tsx +22 -11
- 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 +14 -3
- 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/tool-call-bubble.test.ts +28 -0
- package/src/components/chat/tool-call-bubble.tsx +13 -1
- 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 +7 -6
- 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/input/chat-input.tsx +5 -4
- 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 +23 -9
- package/src/components/logs/log-list.tsx +7 -7
- 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/new-session-sheet.tsx +4 -3
- 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/hooks/use-media-query.ts +30 -4
- package/src/lib/api-client.ts +6 -18
- package/src/lib/fetch-timeout.ts +17 -0
- package/src/lib/notification-sounds.ts +4 -4
- package/src/lib/safe-storage.ts +42 -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-output.test.ts +29 -0
- package/src/lib/server/session-tools/web-output.ts +16 -0
- package/src/lib/server/session-tools/web.ts +7 -3
- 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 +6 -1
- package/src/stores/use-app-store.ts +17 -11
- 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/app/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { initAudioContext } from '@/lib/tts'
|
|
6
6
|
import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
|
|
7
7
|
import { connectWs, disconnectWs } from '@/lib/ws-client'
|
|
8
|
+
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
8
9
|
import { useWs } from '@/hooks/use-ws'
|
|
9
10
|
import { AccessKeyGate } from '@/components/auth/access-key-gate'
|
|
10
11
|
import { UserPicker } from '@/components/auth/user-picker'
|
|
@@ -12,6 +13,8 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
|
|
|
12
13
|
import { AppLayout } from '@/components/layout/app-layout'
|
|
13
14
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
14
15
|
|
|
16
|
+
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
17
|
+
|
|
15
18
|
function FullScreenLoader() {
|
|
16
19
|
return (
|
|
17
20
|
<div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
|
|
@@ -158,11 +161,11 @@ export default function Home() {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
try {
|
|
161
|
-
const res = await
|
|
164
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
162
165
|
method: 'POST',
|
|
163
166
|
headers: { 'Content-Type': 'application/json' },
|
|
164
167
|
body: JSON.stringify({ key }),
|
|
165
|
-
})
|
|
168
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
166
169
|
if (res.ok) {
|
|
167
170
|
setAuthenticated(true)
|
|
168
171
|
} else {
|
|
@@ -171,8 +174,9 @@ export default function Home() {
|
|
|
171
174
|
}
|
|
172
175
|
} catch {
|
|
173
176
|
setAuthenticated(true)
|
|
177
|
+
} finally {
|
|
178
|
+
setAuthChecked(true)
|
|
174
179
|
}
|
|
175
|
-
setAuthChecked(true)
|
|
176
180
|
}, [])
|
|
177
181
|
|
|
178
182
|
// After auth, try to restore username from server settings
|
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">
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
4
|
import { setStoredAccessKey } from '@/lib/api-client'
|
|
5
|
+
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
5
6
|
|
|
6
7
|
interface AccessKeyGateProps {
|
|
7
8
|
onAuthenticated: () => void
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
12
|
+
|
|
10
13
|
export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
11
14
|
const [key, setKey] = useState('')
|
|
12
15
|
const [error, setError] = useState('')
|
|
@@ -19,16 +22,22 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
19
22
|
const [copied, setCopied] = useState(false)
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let cancelled = false
|
|
26
|
+
;(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetchWithTimeout('/api/auth', {}, AUTH_CHECK_TIMEOUT_MS)
|
|
29
|
+
const data = await res.json().catch(() => ({}))
|
|
30
|
+
if (!cancelled && data.firstTime && data.key) {
|
|
26
31
|
setFirstTime(true)
|
|
27
32
|
setGeneratedKey(data.key)
|
|
28
33
|
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Auth check failed:', err)
|
|
36
|
+
} finally {
|
|
37
|
+
if (!cancelled) setChecking(false)
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
return () => { cancelled = true }
|
|
32
41
|
}, [])
|
|
33
42
|
|
|
34
43
|
const handleCopyKey = async () => {
|
|
@@ -44,14 +53,16 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
44
53
|
const handleClaimKey = async () => {
|
|
45
54
|
setLoading(true)
|
|
46
55
|
try {
|
|
47
|
-
const res = await
|
|
56
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
48
57
|
method: 'POST',
|
|
49
58
|
headers: { 'Content-Type': 'application/json' },
|
|
50
59
|
body: JSON.stringify({ key: generatedKey }),
|
|
51
|
-
})
|
|
60
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
52
61
|
if (res.ok) {
|
|
53
62
|
setStoredAccessKey(generatedKey)
|
|
54
63
|
onAuthenticated()
|
|
64
|
+
} else {
|
|
65
|
+
setError('Invalid access key')
|
|
55
66
|
}
|
|
56
67
|
} catch {
|
|
57
68
|
setError('Connection failed')
|
|
@@ -69,11 +80,11 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
69
80
|
setError('')
|
|
70
81
|
|
|
71
82
|
try {
|
|
72
|
-
const res = await
|
|
83
|
+
const res = await fetchWithTimeout('/api/auth', {
|
|
73
84
|
method: 'POST',
|
|
74
85
|
headers: { 'Content-Type': 'application/json' },
|
|
75
86
|
body: JSON.stringify({ key: trimmed }),
|
|
76
|
-
})
|
|
87
|
+
}, AUTH_CHECK_TIMEOUT_MS)
|
|
77
88
|
if (res.ok) {
|
|
78
89
|
setStoredAccessKey(trimmed)
|
|
79
90
|
onAuthenticated()
|
|
@@ -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) => {
|