@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -1,29 +1,71 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
5
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
6
|
-
import type { AgentWallet, WalletChain } from '@/types'
|
|
6
|
+
import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary, WalletChain } from '@/types'
|
|
7
7
|
import { toast } from 'sonner'
|
|
8
|
+
import {
|
|
9
|
+
SUPPORTED_WALLET_CHAINS,
|
|
10
|
+
formatWalletAmount,
|
|
11
|
+
getWalletBalanceAtomic,
|
|
12
|
+
getWalletChainMeta,
|
|
13
|
+
getWalletLimitAtomic,
|
|
14
|
+
} from '@/lib/wallet'
|
|
15
|
+
|
|
16
|
+
type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
|
|
17
|
+
balanceAtomic?: string
|
|
18
|
+
balanceLamports?: number
|
|
19
|
+
balanceFormatted?: string
|
|
20
|
+
balanceSymbol?: string
|
|
21
|
+
assets?: WalletAssetBalance[]
|
|
22
|
+
portfolioSummary?: WalletPortfolioSummary
|
|
23
|
+
isActive?: boolean
|
|
24
|
+
}
|
|
8
25
|
|
|
9
26
|
interface WalletSectionProps {
|
|
10
27
|
agentId: string
|
|
11
|
-
|
|
28
|
+
wallets: SafeWallet[]
|
|
29
|
+
activeWalletId: string | null
|
|
12
30
|
onWalletCreated: () => void
|
|
13
31
|
}
|
|
14
32
|
|
|
15
|
-
export function WalletSection({ agentId,
|
|
33
|
+
export function WalletSection({ agentId, wallets, activeWalletId, onWalletCreated }: WalletSectionProps) {
|
|
16
34
|
const [creating, setCreating] = useState(false)
|
|
35
|
+
const [activatingWalletId, setActivatingWalletId] = useState<string | null>(null)
|
|
17
36
|
const [error, setError] = useState<string | null>(null)
|
|
18
|
-
const [
|
|
37
|
+
const [copiedWalletId, setCopiedWalletId] = useState<string | null>(null)
|
|
38
|
+
|
|
39
|
+
const connectedChains = useMemo(() => new Set(wallets.map((wallet) => wallet.chain)), [wallets])
|
|
40
|
+
const availableChains = useMemo(
|
|
41
|
+
() => SUPPORTED_WALLET_CHAINS.filter((chain) => !connectedChains.has(chain)),
|
|
42
|
+
[connectedChains],
|
|
43
|
+
)
|
|
44
|
+
const sortedWallets = useMemo(
|
|
45
|
+
() => [...wallets].sort((a, b) => {
|
|
46
|
+
const aActive = a.id === activeWalletId || a.isActive === true
|
|
47
|
+
const bActive = b.id === activeWalletId || b.isActive === true
|
|
48
|
+
if (aActive !== bActive) return aActive ? -1 : 1
|
|
49
|
+
return a.chain.localeCompare(b.chain)
|
|
50
|
+
}),
|
|
51
|
+
[activeWalletId, wallets],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const [chain, setChain] = useState<WalletChain>(availableChains[0] || 'solana')
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (availableChains.length === 0) return
|
|
58
|
+
if (!availableChains.includes(chain)) setChain(availableChains[0])
|
|
59
|
+
}, [availableChains, chain])
|
|
19
60
|
|
|
20
61
|
const createWallet = useCallback(async () => {
|
|
62
|
+
if (!availableChains.includes(chain)) return
|
|
21
63
|
setCreating(true)
|
|
22
64
|
setError(null)
|
|
23
65
|
try {
|
|
24
|
-
await api('POST', '/wallets', { agentId, chain
|
|
66
|
+
await api('POST', '/wallets', { agentId, chain })
|
|
25
67
|
toast.success('Agent wallet created successfully')
|
|
26
|
-
onWalletCreated()
|
|
68
|
+
await onWalletCreated()
|
|
27
69
|
} catch (err: unknown) {
|
|
28
70
|
const msg = err instanceof Error ? err.message : String(err)
|
|
29
71
|
setError(msg)
|
|
@@ -31,32 +73,165 @@ export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectio
|
|
|
31
73
|
} finally {
|
|
32
74
|
setCreating(false)
|
|
33
75
|
}
|
|
34
|
-
}, [agentId, onWalletCreated])
|
|
76
|
+
}, [agentId, availableChains, chain, onWalletCreated])
|
|
35
77
|
|
|
36
|
-
const copyAddress = useCallback(async () => {
|
|
37
|
-
if (!wallet) return
|
|
78
|
+
const copyAddress = useCallback(async (wallet: SafeWallet) => {
|
|
38
79
|
const copiedValue = await copyTextToClipboard(wallet.publicKey)
|
|
39
80
|
if (!copiedValue) return
|
|
40
|
-
|
|
41
|
-
setTimeout(() =>
|
|
42
|
-
|
|
81
|
+
setCopiedWalletId(wallet.id)
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
setCopiedWalletId((current) => current === wallet.id ? null : current)
|
|
84
|
+
}, 2000)
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
const setActiveWallet = useCallback(async (walletId: string) => {
|
|
88
|
+
setActivatingWalletId(walletId)
|
|
89
|
+
setError(null)
|
|
90
|
+
try {
|
|
91
|
+
await api('PATCH', `/wallets/${walletId}`, { makeActive: true })
|
|
92
|
+
toast.success('Default wallet updated')
|
|
93
|
+
await onWalletCreated()
|
|
94
|
+
} catch (err: unknown) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
96
|
+
setError(msg)
|
|
97
|
+
toast.error(msg)
|
|
98
|
+
} finally {
|
|
99
|
+
setActivatingWalletId(null)
|
|
100
|
+
}
|
|
101
|
+
}, [onWalletCreated])
|
|
43
102
|
|
|
44
103
|
return (
|
|
45
104
|
<div className="mb-8">
|
|
46
105
|
<div className="flex items-center gap-2 mb-3">
|
|
47
106
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
48
|
-
|
|
107
|
+
Wallets
|
|
49
108
|
</label>
|
|
50
109
|
<span className="px-1.5 py-0.5 rounded-[4px] bg-amber-500/15 text-amber-400 text-[9px] font-600 uppercase tracking-wide">
|
|
51
110
|
Experimental
|
|
52
111
|
</span>
|
|
53
112
|
</div>
|
|
54
113
|
|
|
55
|
-
{
|
|
56
|
-
<div className="
|
|
114
|
+
{sortedWallets.length > 0 ? (
|
|
115
|
+
<div className="space-y-3">
|
|
116
|
+
<div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
|
|
117
|
+
<div className="flex items-center justify-between gap-3 mb-2">
|
|
118
|
+
<div>
|
|
119
|
+
<div className="text-[12px] font-600 text-text-1">Combined Wallet Summary</div>
|
|
120
|
+
<p className="text-[11px] text-text-3/70 mt-1">
|
|
121
|
+
{sortedWallets.length} wallet{sortedWallets.length === 1 ? '' : 's'} connected. The active wallet is used by default when the agent does not specify a chain explicitly.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="text-right">
|
|
125
|
+
<div className="text-[18px] font-600 text-text-1">{sortedWallets.length}</div>
|
|
126
|
+
<div className="text-[10px] uppercase tracking-wide text-text-3/50">Connected</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
130
|
+
{sortedWallets.map((wallet) => {
|
|
131
|
+
const walletMeta = getWalletChainMeta(wallet.chain)
|
|
132
|
+
const balanceFormatted = wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 4, maxFractionDigits: 6 })
|
|
133
|
+
const isActive = wallet.id === activeWalletId || wallet.isActive === true
|
|
134
|
+
return (
|
|
135
|
+
<div key={wallet.id} className="rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">{walletMeta.label}</span>
|
|
138
|
+
{isActive && (
|
|
139
|
+
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft text-accent-bright text-[9px] font-600 uppercase tracking-wide">
|
|
140
|
+
Active
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
<div className="mt-1 text-[14px] font-600 text-text-1">{balanceFormatted} {walletMeta.symbol}</div>
|
|
145
|
+
{wallet.portfolioSummary?.nonZeroAssets ? (
|
|
146
|
+
<div className="mt-1 text-[10px] text-text-3/55">
|
|
147
|
+
{wallet.portfolioSummary.nonZeroAssets} asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} tracked
|
|
148
|
+
</div>
|
|
149
|
+
) : null}
|
|
150
|
+
<div className="mt-1 text-[10px] text-text-3/55 font-mono truncate">{wallet.publicKey}</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{sortedWallets.map((wallet) => {
|
|
158
|
+
const walletMeta = getWalletChainMeta(wallet.chain)
|
|
159
|
+
const balanceFormatted = wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 4, maxFractionDigits: 6 })
|
|
160
|
+
const perTxLimit = formatWalletAmount(wallet.chain, getWalletLimitAtomic(wallet, 'perTx'), { maxFractionDigits: 6 })
|
|
161
|
+
const dailyLimit = formatWalletAmount(wallet.chain, getWalletLimitAtomic(wallet, 'daily'), { maxFractionDigits: 6 })
|
|
162
|
+
const isActive = wallet.id === activeWalletId || wallet.isActive === true
|
|
163
|
+
return (
|
|
164
|
+
<div key={wallet.id} className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50 space-y-3">
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
|
|
167
|
+
{walletMeta.label}
|
|
168
|
+
</span>
|
|
169
|
+
{isActive && (
|
|
170
|
+
<span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft text-accent-bright text-[9px] font-600 uppercase tracking-wide">
|
|
171
|
+
Default
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
<span className="flex-1" />
|
|
175
|
+
<span className="text-[13px] font-600 text-text-1">
|
|
176
|
+
{balanceFormatted} {walletMeta.symbol}
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
<code className="text-[11px] text-text-3 bg-black/20 px-2 py-1 rounded-[6px] font-mono truncate flex-1">
|
|
181
|
+
{wallet.publicKey}
|
|
182
|
+
</code>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => copyAddress(wallet)}
|
|
186
|
+
className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
|
|
187
|
+
style={{ fontFamily: 'inherit' }}
|
|
188
|
+
>
|
|
189
|
+
{copiedWalletId === wallet.id ? 'Copied!' : 'Copy'}
|
|
190
|
+
</button>
|
|
191
|
+
{!isActive && (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => setActiveWallet(wallet.id)}
|
|
195
|
+
disabled={activatingWalletId === wallet.id}
|
|
196
|
+
className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] font-600 text-accent-bright border border-accent-bright/20 bg-accent-soft/10 hover:bg-accent-soft/20 transition-colors cursor-pointer disabled:opacity-50"
|
|
197
|
+
style={{ fontFamily: 'inherit' }}
|
|
198
|
+
>
|
|
199
|
+
{activatingWalletId === wallet.id ? 'Setting...' : 'Set Default'}
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
<div className="flex flex-wrap items-center gap-3 text-[10px] text-text-3/60">
|
|
204
|
+
<span>Limit: {perTxLimit} {walletMeta.symbol}/tx</span>
|
|
205
|
+
<span>Daily: {dailyLimit} {walletMeta.symbol}</span>
|
|
206
|
+
<span>{wallet.requireApproval ? 'Approval required' : 'Auto-send'}</span>
|
|
207
|
+
{wallet.portfolioSummary?.nonZeroAssets ? (
|
|
208
|
+
<span>{wallet.portfolioSummary.nonZeroAssets} asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} detected</span>
|
|
209
|
+
) : null}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
) : null}
|
|
216
|
+
|
|
217
|
+
{availableChains.length > 0 ? (
|
|
218
|
+
<div className="mt-3 p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
|
|
57
219
|
<p className="text-[12px] text-text-3/70 mb-3">
|
|
58
|
-
|
|
220
|
+
{getWalletChainMeta(chain).createDescription}
|
|
59
221
|
</p>
|
|
222
|
+
<label className="block text-[11px] text-text-3/70 mb-1">Wallet Type</label>
|
|
223
|
+
<select
|
|
224
|
+
value={chain}
|
|
225
|
+
onChange={(event) => setChain(event.target.value as WalletChain)}
|
|
226
|
+
className="w-full mb-3 px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
|
|
227
|
+
style={{ fontFamily: 'inherit' }}
|
|
228
|
+
>
|
|
229
|
+
{availableChains.map((availableChain) => (
|
|
230
|
+
<option key={availableChain} value={availableChain}>
|
|
231
|
+
{getWalletChainMeta(availableChain).label}
|
|
232
|
+
</option>
|
|
233
|
+
))}
|
|
234
|
+
</select>
|
|
60
235
|
<button
|
|
61
236
|
type="button"
|
|
62
237
|
onClick={createWallet}
|
|
@@ -64,41 +239,16 @@ export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectio
|
|
|
64
239
|
className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[11px] font-600 hover:bg-accent-bright/15 transition-all cursor-pointer disabled:opacity-50 border border-accent-bright/20"
|
|
65
240
|
style={{ fontFamily: 'inherit' }}
|
|
66
241
|
>
|
|
67
|
-
{creating ? 'Creating...' :
|
|
242
|
+
{creating ? 'Creating...' : `Create ${getWalletChainMeta(chain).label} Wallet`}
|
|
68
243
|
</button>
|
|
69
244
|
{error && <p className="text-[11px] text-red-400 mt-2">{error}</p>}
|
|
70
245
|
</div>
|
|
71
246
|
) : (
|
|
72
|
-
<div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<span className="flex-1" />
|
|
78
|
-
{typeof wallet.balanceSol === 'number' && (
|
|
79
|
-
<span className="text-[13px] font-600 text-text-1">
|
|
80
|
-
{wallet.balanceSol.toFixed(4)} SOL
|
|
81
|
-
</span>
|
|
82
|
-
)}
|
|
83
|
-
</div>
|
|
84
|
-
<div className="flex items-center gap-2">
|
|
85
|
-
<code className="text-[11px] text-text-3 bg-black/20 px-2 py-1 rounded-[6px] font-mono truncate flex-1">
|
|
86
|
-
{wallet.publicKey}
|
|
87
|
-
</code>
|
|
88
|
-
<button
|
|
89
|
-
type="button"
|
|
90
|
-
onClick={copyAddress}
|
|
91
|
-
className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
|
|
92
|
-
style={{ fontFamily: 'inherit' }}
|
|
93
|
-
>
|
|
94
|
-
{copied ? 'Copied!' : 'Copy'}
|
|
95
|
-
</button>
|
|
96
|
-
</div>
|
|
97
|
-
<div className="flex items-center gap-3 text-[10px] text-text-3/60">
|
|
98
|
-
<span>Limit: {((wallet.spendingLimitLamports ?? 100_000_000) / 1e9).toFixed(2)} SOL/tx</span>
|
|
99
|
-
<span>Daily: {((wallet.dailyLimitLamports ?? 1_000_000_000) / 1e9).toFixed(1)} SOL</span>
|
|
100
|
-
<span>{wallet.requireApproval ? 'Approval required' : 'Auto-send'}</span>
|
|
101
|
-
</div>
|
|
247
|
+
<div className="mt-3 p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
|
|
248
|
+
<p className="text-[12px] text-text-3/70">
|
|
249
|
+
This agent already has both supported wallet types connected. Use the default toggle above to choose which wallet autonomous actions use when no chain is specified.
|
|
250
|
+
</p>
|
|
251
|
+
{error && <p className="text-[11px] text-red-400 mt-2">{error}</p>}
|
|
102
252
|
</div>
|
|
103
253
|
)}
|
|
104
254
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
7
8
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
8
9
|
import type { Webhook, WebhookLogEntry } from '@/types'
|
|
@@ -61,6 +62,8 @@ export function WebhookSheet() {
|
|
|
61
62
|
const [tab, setTab] = useState<'config' | 'history'>('config')
|
|
62
63
|
const [history, setHistory] = useState<WebhookLogEntry[]>([])
|
|
63
64
|
const [historyLoading, setHistoryLoading] = useState(false)
|
|
65
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
66
|
+
const [deleting, setDeleting] = useState(false)
|
|
64
67
|
|
|
65
68
|
const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
|
|
66
69
|
const endpoint = editing ? webhookUrl(editing.id) : ''
|
|
@@ -109,6 +112,8 @@ export function WebhookSheet() {
|
|
|
109
112
|
}, [editing, open])
|
|
110
113
|
|
|
111
114
|
const handleClose = () => {
|
|
115
|
+
setConfirmDelete(false)
|
|
116
|
+
setDeleting(false)
|
|
112
117
|
setOpen(false)
|
|
113
118
|
setEditingId(null)
|
|
114
119
|
}
|
|
@@ -164,17 +169,21 @@ export function WebhookSheet() {
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
const handleDelete = async () => {
|
|
167
|
-
if (!editing
|
|
172
|
+
if (!editing) return
|
|
173
|
+
setDeleting(true)
|
|
168
174
|
try {
|
|
169
175
|
const res = await api<DeleteWebhookResponse>('DELETE', `/webhooks/${editing.id}`)
|
|
170
176
|
if ('error' in res && res.error) throw new Error(res.error)
|
|
171
177
|
toast.success('Webhook deleted')
|
|
172
178
|
await loadWebhooks()
|
|
179
|
+
setConfirmDelete(false)
|
|
173
180
|
handleClose()
|
|
174
181
|
} catch (err: unknown) {
|
|
175
182
|
const msg = err instanceof Error ? err.message : 'Failed to delete webhook'
|
|
176
183
|
setError(msg)
|
|
177
184
|
toast.error(msg)
|
|
185
|
+
} finally {
|
|
186
|
+
setDeleting(false)
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
|
|
@@ -381,7 +390,7 @@ export function WebhookSheet() {
|
|
|
381
390
|
<div className="flex gap-3 pt-2">
|
|
382
391
|
{editing && (
|
|
383
392
|
<button
|
|
384
|
-
onClick={
|
|
393
|
+
onClick={() => setConfirmDelete(true)}
|
|
385
394
|
className="px-5 py-3 rounded-[14px] border border-danger/30 bg-transparent text-danger text-[14px] font-600 cursor-pointer hover:bg-danger/10 transition-colors"
|
|
386
395
|
style={{ fontFamily: 'inherit' }}
|
|
387
396
|
>
|
|
@@ -407,6 +416,17 @@ export function WebhookSheet() {
|
|
|
407
416
|
</div>
|
|
408
417
|
</>}
|
|
409
418
|
</div>
|
|
419
|
+
<ConfirmDialog
|
|
420
|
+
open={confirmDelete}
|
|
421
|
+
title="Delete Webhook?"
|
|
422
|
+
message={editing ? `Delete "${editing.name}"? This will remove the endpoint and its saved webhook configuration.` : 'Delete this webhook?'}
|
|
423
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
424
|
+
confirmDisabled={deleting}
|
|
425
|
+
cancelDisabled={deleting}
|
|
426
|
+
danger
|
|
427
|
+
onConfirm={() => { void handleDelete() }}
|
|
428
|
+
onCancel={() => { if (!deleting) setConfirmDelete(false) }}
|
|
429
|
+
/>
|
|
410
430
|
</BottomSheet>
|
|
411
431
|
)
|
|
412
432
|
}
|
|
@@ -36,6 +36,14 @@ export function getApprovalTitle(approval: ApprovalRequest): string {
|
|
|
36
36
|
const pluginId = getApprovalPluginId(approval)
|
|
37
37
|
return pluginId ? `Enable Plugin: ${pluginId}` : 'Enable Plugin'
|
|
38
38
|
}
|
|
39
|
+
if (approval.category === 'connector_sender') {
|
|
40
|
+
const data = dataObject(approval)
|
|
41
|
+
const senderName = typeof data.senderName === 'string' ? data.senderName.trim() : ''
|
|
42
|
+
const senderId = typeof data.senderId === 'string' ? data.senderId.trim() : ''
|
|
43
|
+
const connectorName = typeof data.connectorName === 'string' ? data.connectorName.trim() : ''
|
|
44
|
+
const subject = senderName || senderId || 'Unknown sender'
|
|
45
|
+
return connectorName ? `Approve ${subject} on ${connectorName}` : `Approve ${subject}`
|
|
46
|
+
}
|
|
39
47
|
return approval.title || 'Approval Request'
|
|
40
48
|
}
|
|
41
49
|
|
|
@@ -58,5 +66,17 @@ export function getApprovalPayload(approval: ApprovalRequest): Record<string, un
|
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
if (approval.category === 'connector_sender') {
|
|
70
|
+
return {
|
|
71
|
+
connectorId: typeof data.connectorId === 'string' ? data.connectorId : null,
|
|
72
|
+
connectorName: typeof data.connectorName === 'string' ? data.connectorName : null,
|
|
73
|
+
platform: typeof data.platform === 'string' ? data.platform : null,
|
|
74
|
+
senderId: typeof data.senderId === 'string' ? data.senderId : null,
|
|
75
|
+
senderName: typeof data.senderName === 'string' ? data.senderName : null,
|
|
76
|
+
channelId: typeof data.channelId === 'string' ? data.channelId : null,
|
|
77
|
+
policy: typeof data.policy === 'string' ? data.policy : null,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
return (sanitizeValue(data) || {}) as Record<string, unknown>
|
|
62
82
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanvasActionItem,
|
|
3
|
+
CanvasBlock,
|
|
4
|
+
CanvasCardItem,
|
|
5
|
+
CanvasContent,
|
|
6
|
+
CanvasDocument,
|
|
7
|
+
CanvasMetricItem,
|
|
8
|
+
CanvasTableData,
|
|
9
|
+
} from '@/types'
|
|
10
|
+
|
|
11
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
13
|
+
return value as Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function asTrimmedString(value: unknown, max = 8000): string | null {
|
|
17
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value).slice(0, max)
|
|
18
|
+
if (typeof value !== 'string') return null
|
|
19
|
+
const trimmed = value.trim()
|
|
20
|
+
return trimmed ? trimmed.slice(0, max) : null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asStringArray(value: unknown): string[] {
|
|
24
|
+
if (!Array.isArray(value)) return []
|
|
25
|
+
return value
|
|
26
|
+
.map((entry) => asTrimmedString(entry, 240))
|
|
27
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeMetricItems(value: unknown): CanvasMetricItem[] {
|
|
31
|
+
if (!Array.isArray(value)) return []
|
|
32
|
+
const items: CanvasMetricItem[] = []
|
|
33
|
+
for (const entry of value) {
|
|
34
|
+
const row = asObject(entry)
|
|
35
|
+
if (!row) continue
|
|
36
|
+
const label = asTrimmedString(row.label, 120)
|
|
37
|
+
const metricValue = asTrimmedString(row.value, 120)
|
|
38
|
+
if (!label || !metricValue) continue
|
|
39
|
+
items.push({
|
|
40
|
+
label,
|
|
41
|
+
value: metricValue,
|
|
42
|
+
detail: asTrimmedString(row.detail, 240) || undefined,
|
|
43
|
+
tone: row.tone === 'positive' || row.tone === 'negative' || row.tone === 'warning' ? row.tone : 'default',
|
|
44
|
+
})
|
|
45
|
+
if (items.length >= 24) break
|
|
46
|
+
}
|
|
47
|
+
return items
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeCardItems(value: unknown): CanvasCardItem[] {
|
|
51
|
+
if (!Array.isArray(value)) return []
|
|
52
|
+
const items: CanvasCardItem[] = []
|
|
53
|
+
for (const entry of value) {
|
|
54
|
+
const row = asObject(entry)
|
|
55
|
+
if (!row) continue
|
|
56
|
+
const title = asTrimmedString(row.title, 180)
|
|
57
|
+
if (!title) continue
|
|
58
|
+
items.push({
|
|
59
|
+
title,
|
|
60
|
+
body: asTrimmedString(row.body, 1600) || undefined,
|
|
61
|
+
meta: asTrimmedString(row.meta, 200) || undefined,
|
|
62
|
+
tone: row.tone === 'positive' || row.tone === 'negative' || row.tone === 'warning' ? row.tone : 'default',
|
|
63
|
+
})
|
|
64
|
+
if (items.length >= 24) break
|
|
65
|
+
}
|
|
66
|
+
return items
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeActionItems(value: unknown): CanvasActionItem[] {
|
|
70
|
+
if (!Array.isArray(value)) return []
|
|
71
|
+
const items: CanvasActionItem[] = []
|
|
72
|
+
for (const entry of value) {
|
|
73
|
+
const row = asObject(entry)
|
|
74
|
+
if (!row) continue
|
|
75
|
+
const label = asTrimmedString(row.label, 120)
|
|
76
|
+
if (!label) continue
|
|
77
|
+
items.push({
|
|
78
|
+
label,
|
|
79
|
+
href: asTrimmedString(row.href, 1000) || undefined,
|
|
80
|
+
note: asTrimmedString(row.note, 240) || undefined,
|
|
81
|
+
intent: row.intent === 'primary' || row.intent === 'success' || row.intent === 'danger' ? row.intent : 'secondary',
|
|
82
|
+
})
|
|
83
|
+
if (items.length >= 24) break
|
|
84
|
+
}
|
|
85
|
+
return items
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeTable(value: unknown): CanvasTableData | null {
|
|
89
|
+
const row = asObject(value)
|
|
90
|
+
if (!row) return null
|
|
91
|
+
const columns = asStringArray(row.columns).slice(0, 20)
|
|
92
|
+
if (!columns.length) return null
|
|
93
|
+
const rows = Array.isArray(row.rows)
|
|
94
|
+
? row.rows
|
|
95
|
+
.map((entry) => Array.isArray(entry)
|
|
96
|
+
? entry.slice(0, columns.length).map((cell) => (
|
|
97
|
+
typeof cell === 'string'
|
|
98
|
+
|| typeof cell === 'number'
|
|
99
|
+
|| typeof cell === 'boolean'
|
|
100
|
+
|| cell === null
|
|
101
|
+
? cell
|
|
102
|
+
: JSON.stringify(cell)
|
|
103
|
+
))
|
|
104
|
+
: null)
|
|
105
|
+
.filter((entry): entry is Array<string | number | boolean | null> => Array.isArray(entry))
|
|
106
|
+
.slice(0, 100)
|
|
107
|
+
: []
|
|
108
|
+
return rows.length
|
|
109
|
+
? {
|
|
110
|
+
columns,
|
|
111
|
+
rows,
|
|
112
|
+
caption: asTrimmedString(row.caption, 240) || undefined,
|
|
113
|
+
}
|
|
114
|
+
: null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeBlock(value: unknown): CanvasBlock | null {
|
|
118
|
+
const row = asObject(value)
|
|
119
|
+
if (!row) return null
|
|
120
|
+
const title = asTrimmedString(row.title, 160) || undefined
|
|
121
|
+
switch (row.type) {
|
|
122
|
+
case 'markdown': {
|
|
123
|
+
const markdown = asTrimmedString(row.markdown, 20_000)
|
|
124
|
+
return markdown ? { type: 'markdown', title, markdown } : null
|
|
125
|
+
}
|
|
126
|
+
case 'metrics': {
|
|
127
|
+
const items = normalizeMetricItems(row.items)
|
|
128
|
+
return items.length ? { type: 'metrics', title, items } : null
|
|
129
|
+
}
|
|
130
|
+
case 'cards': {
|
|
131
|
+
const items = normalizeCardItems(row.items)
|
|
132
|
+
return items.length ? { type: 'cards', title, items } : null
|
|
133
|
+
}
|
|
134
|
+
case 'table': {
|
|
135
|
+
const table = normalizeTable(row.table)
|
|
136
|
+
return table ? { type: 'table', title, table } : null
|
|
137
|
+
}
|
|
138
|
+
case 'code': {
|
|
139
|
+
const code = asTrimmedString(row.code, 20_000)
|
|
140
|
+
return code ? { type: 'code', title, code, language: asTrimmedString(row.language, 60) || undefined } : null
|
|
141
|
+
}
|
|
142
|
+
case 'actions': {
|
|
143
|
+
const items = normalizeActionItems(row.items)
|
|
144
|
+
return items.length ? { type: 'actions', title, items } : null
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function normalizeCanvasDocument(value: unknown): CanvasDocument | null {
|
|
152
|
+
const row = asObject(value)
|
|
153
|
+
if (!row) return null
|
|
154
|
+
const blocks = Array.isArray(row.blocks)
|
|
155
|
+
? row.blocks.map((entry) => normalizeBlock(entry)).filter((entry): entry is CanvasBlock => entry !== null).slice(0, 24)
|
|
156
|
+
: []
|
|
157
|
+
if (!blocks.length) return null
|
|
158
|
+
return {
|
|
159
|
+
kind: 'structured',
|
|
160
|
+
title: asTrimmedString(row.title, 180) || undefined,
|
|
161
|
+
subtitle: asTrimmedString(row.subtitle, 320) || undefined,
|
|
162
|
+
theme: row.theme === 'sky' || row.theme === 'emerald' || row.theme === 'amber' || row.theme === 'rose' ? row.theme : 'slate',
|
|
163
|
+
blocks,
|
|
164
|
+
updatedAt: typeof row.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? row.updatedAt : Date.now(),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isCanvasDocument(value: unknown): value is CanvasDocument {
|
|
169
|
+
return normalizeCanvasDocument(value) !== null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function normalizeCanvasContent(value: unknown): CanvasContent {
|
|
173
|
+
if (typeof value === 'string') return value || null
|
|
174
|
+
if (value === null || value === undefined) return null
|
|
175
|
+
return normalizeCanvasDocument(value)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function summarizeCanvasContent(content: CanvasContent): Record<string, unknown> {
|
|
179
|
+
if (!content) {
|
|
180
|
+
return { kind: 'empty', hasContent: false, contentLength: 0, preview: null }
|
|
181
|
+
}
|
|
182
|
+
if (typeof content === 'string') {
|
|
183
|
+
return {
|
|
184
|
+
kind: 'html',
|
|
185
|
+
hasContent: true,
|
|
186
|
+
contentLength: content.length,
|
|
187
|
+
preview: content.slice(0, 500),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
kind: 'structured',
|
|
192
|
+
hasContent: true,
|
|
193
|
+
blockCount: content.blocks.length,
|
|
194
|
+
title: content.title || null,
|
|
195
|
+
blockTypes: content.blocks.map((block) => block.type),
|
|
196
|
+
preview: JSON.stringify(content).slice(0, 500),
|
|
197
|
+
}
|
|
198
|
+
}
|