@swarmclawai/swarmclaw 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -4,6 +4,7 @@ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
|
4
4
|
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
|
5
5
|
import type { Agent } from '@/types'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
7
8
|
import { AgentAvatar } from './agent-avatar'
|
|
8
9
|
import { AgentFilesEditor } from './agent-files-editor'
|
|
9
10
|
import { OpenClawSkillsPanel } from './openclaw-skills-panel'
|
|
@@ -11,6 +12,7 @@ import { PermissionPresetSelector } from './permission-preset-selector'
|
|
|
11
12
|
import { ExecConfigPanel } from './exec-config-panel'
|
|
12
13
|
import { SandboxEnvPanel } from './sandbox-env-panel'
|
|
13
14
|
import { CronJobForm } from './cron-job-form'
|
|
15
|
+
import { toast } from 'sonner'
|
|
14
16
|
|
|
15
17
|
interface Props {
|
|
16
18
|
agent: Agent
|
|
@@ -87,6 +89,12 @@ export function InspectorPanel({ agent, onEditAgent, onClearHistory, onDeleteAge
|
|
|
87
89
|
<div className="min-w-0 flex-1">
|
|
88
90
|
<div className="flex items-center gap-2 min-w-0">
|
|
89
91
|
<h3 className="font-display text-[16px] font-700 text-text truncate tracking-[-0.02em]">{agent.name}</h3>
|
|
92
|
+
{agent.disabled === true && (
|
|
93
|
+
<span className="inline-flex items-center gap-1 rounded-[7px] border border-amber-400/15 bg-amber-400/[0.1] px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-amber-300">
|
|
94
|
+
<span className="w-1.5 h-1.5 rounded-full bg-amber-300" />
|
|
95
|
+
Disabled
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
90
98
|
{agent.heartbeatEnabled && (
|
|
91
99
|
<span className="inline-flex items-center gap-1 rounded-[7px] border border-emerald-400/15 bg-emerald-400/10 px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.12em] text-emerald-300">
|
|
92
100
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
@@ -189,13 +197,32 @@ interface OverviewTabProps {
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: OverviewTabProps) {
|
|
200
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
201
|
+
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
202
|
+
const [availabilitySaving, setAvailabilitySaving] = useState(false)
|
|
192
203
|
const summaryStats = [
|
|
193
204
|
{ label: 'Provider', value: PROVIDER_LABELS[agent.provider] || agent.provider.replace(/-/g, ' ') },
|
|
194
205
|
{ label: 'Model', value: agent.model || 'Default' },
|
|
195
206
|
{ label: 'Plugins', value: String(agent.plugins?.length ?? 0) },
|
|
196
207
|
{ label: 'Heartbeat', value: agent.heartbeatEnabled ? `Every ${agent.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC}s` : 'Off' },
|
|
208
|
+
{ label: 'Status', value: agent.disabled === true ? 'Disabled' : 'Enabled' },
|
|
197
209
|
]
|
|
198
210
|
|
|
211
|
+
const handleToggleAvailability = useCallback(async () => {
|
|
212
|
+
if (availabilitySaving) return
|
|
213
|
+
setAvailabilitySaving(true)
|
|
214
|
+
try {
|
|
215
|
+
const nextDisabled = agent.disabled !== true
|
|
216
|
+
await api('PUT', `/agents/${agent.id}`, { disabled: nextDisabled })
|
|
217
|
+
await Promise.all([loadAgents(), loadSessions()])
|
|
218
|
+
toast.success(nextDisabled ? `${agent.name} disabled` : `${agent.name} enabled`)
|
|
219
|
+
} catch (err: unknown) {
|
|
220
|
+
toast.error(err instanceof Error ? err.message : 'Failed to update agent availability')
|
|
221
|
+
} finally {
|
|
222
|
+
setAvailabilitySaving(false)
|
|
223
|
+
}
|
|
224
|
+
}, [agent.disabled, agent.id, agent.name, availabilitySaving, loadAgents, loadSessions])
|
|
225
|
+
|
|
199
226
|
return (
|
|
200
227
|
<div className="p-4 flex flex-col gap-4">
|
|
201
228
|
<div className={panelCardClass('p-4 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]')}>
|
|
@@ -259,6 +286,20 @@ function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDele
|
|
|
259
286
|
Edit Agent
|
|
260
287
|
</button>
|
|
261
288
|
)}
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => void handleToggleAvailability()}
|
|
291
|
+
disabled={availabilitySaving}
|
|
292
|
+
className={`w-full px-3 py-2.5 rounded-[10px] text-[12px] font-700 border cursor-pointer transition-all text-left disabled:opacity-50 ${
|
|
293
|
+
agent.disabled === true
|
|
294
|
+
? 'text-emerald-300 bg-emerald-400/[0.06] border-emerald-400/[0.12] hover:bg-emerald-400/[0.1]'
|
|
295
|
+
: 'text-amber-300 bg-amber-400/[0.06] border-amber-400/[0.12] hover:bg-amber-400/[0.1]'
|
|
296
|
+
}`}
|
|
297
|
+
style={{ fontFamily: 'inherit' }}
|
|
298
|
+
>
|
|
299
|
+
{availabilitySaving
|
|
300
|
+
? (agent.disabled === true ? 'Enabling Agent...' : 'Disabling Agent...')
|
|
301
|
+
: (agent.disabled === true ? 'Enable Agent' : 'Disable Agent')}
|
|
302
|
+
</button>
|
|
262
303
|
{(onClearHistory || onDeleteAgent || onDeleteChat) && (
|
|
263
304
|
<>
|
|
264
305
|
<SectionLabel>Danger Zone</SectionLabel>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
4
5
|
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
7
|
+
import { normalizeCanvasContent } from '@/lib/canvas-content'
|
|
8
|
+
import type { CanvasContent, CanvasDocument } from '@/types'
|
|
6
9
|
|
|
7
10
|
interface CanvasPanelProps {
|
|
8
11
|
sessionId: string
|
|
@@ -10,86 +13,254 @@ interface CanvasPanelProps {
|
|
|
10
13
|
onClose: () => void
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
const THEME_STYLES: Record<NonNullable<CanvasDocument['theme']>, { accent: string; chip: string }> = {
|
|
17
|
+
slate: { accent: 'text-sky-300', chip: 'bg-sky-500/10 text-sky-300 border-sky-500/20' },
|
|
18
|
+
sky: { accent: 'text-sky-300', chip: 'bg-sky-500/10 text-sky-300 border-sky-500/20' },
|
|
19
|
+
emerald: { accent: 'text-emerald-300', chip: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20' },
|
|
20
|
+
amber: { accent: 'text-amber-300', chip: 'bg-amber-500/10 text-amber-300 border-amber-500/20' },
|
|
21
|
+
rose: { accent: 'text-rose-300', chip: 'bg-rose-500/10 text-rose-300 border-rose-500/20' },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toneClass(tone?: string): string {
|
|
25
|
+
switch (tone) {
|
|
26
|
+
case 'positive': return 'text-emerald-300'
|
|
27
|
+
case 'negative': return 'text-rose-300'
|
|
28
|
+
case 'warning': return 'text-amber-300'
|
|
29
|
+
default: return 'text-text'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function intentClass(intent?: string): string {
|
|
34
|
+
switch (intent) {
|
|
35
|
+
case 'primary': return 'bg-sky-500 text-white border-sky-400/30'
|
|
36
|
+
case 'success': return 'bg-emerald-500 text-white border-emerald-400/30'
|
|
37
|
+
case 'danger': return 'bg-rose-500 text-white border-rose-400/30'
|
|
38
|
+
default: return 'bg-white/[0.03] text-text-2 border-white/[0.08]'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StructuredCanvasView({ document }: { document: CanvasDocument }) {
|
|
43
|
+
const theme = THEME_STYLES[document.theme || 'slate']
|
|
44
|
+
return (
|
|
45
|
+
<div className="h-full overflow-y-auto bg-bg px-5 py-5">
|
|
46
|
+
<div className="max-w-4xl mx-auto space-y-4">
|
|
47
|
+
{(document.title || document.subtitle) && (
|
|
48
|
+
<div className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
49
|
+
{document.title && <h2 className={`font-display text-[22px] font-700 tracking-[-0.03em] ${theme.accent}`}>{document.title}</h2>}
|
|
50
|
+
{document.subtitle && <p className="mt-1 text-[13px] text-text-3/70">{document.subtitle}</p>}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{document.blocks.map((block, index) => {
|
|
55
|
+
if (block.type === 'markdown') {
|
|
56
|
+
return (
|
|
57
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
58
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
59
|
+
<div className="max-w-none text-[14px] leading-6 text-text-2/90 [&_h1]:font-display [&_h1]:text-[24px] [&_h1]:text-text [&_h2]:font-display [&_h2]:text-[20px] [&_h2]:text-text [&_h3]:font-display [&_h3]:text-[18px] [&_h3]:text-text [&_p]:my-3 [&_ul]:my-3 [&_ul]:pl-5 [&_li]:my-1 [&_code]:rounded [&_code]:bg-black/[0.2] [&_code]:px-1.5 [&_code]:py-0.5">
|
|
60
|
+
<ReactMarkdown>{block.markdown}</ReactMarkdown>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (block.type === 'metrics') {
|
|
67
|
+
return (
|
|
68
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
69
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
70
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
|
|
71
|
+
{block.items.map((item) => (
|
|
72
|
+
<div key={item.label} className="rounded-[14px] border border-white/[0.08] bg-black/[0.14] px-4 py-3">
|
|
73
|
+
<div className="text-[11px] uppercase tracking-[0.08em] text-text-3/60">{item.label}</div>
|
|
74
|
+
<div className={`mt-1 text-[24px] font-display font-700 tracking-[-0.03em] ${toneClass(item.tone)}`}>{item.value}</div>
|
|
75
|
+
{item.detail && <div className="mt-1 text-[12px] text-text-3/65">{item.detail}</div>}
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (block.type === 'cards') {
|
|
84
|
+
return (
|
|
85
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
86
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
87
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
88
|
+
{block.items.map((item) => (
|
|
89
|
+
<div key={item.title} className="rounded-[14px] border border-white/[0.08] bg-black/[0.14] px-4 py-3">
|
|
90
|
+
<div className={`text-[15px] font-700 ${toneClass(item.tone)}`}>{item.title}</div>
|
|
91
|
+
{item.body && <p className="mt-2 text-[13px] leading-6 text-text-2/85 whitespace-pre-wrap">{item.body}</p>}
|
|
92
|
+
{item.meta && <div className="mt-3 text-[11px] text-text-3/60">{item.meta}</div>}
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (block.type === 'table') {
|
|
101
|
+
return (
|
|
102
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4 overflow-hidden">
|
|
103
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
104
|
+
<div className="overflow-x-auto rounded-[12px] border border-white/[0.08]">
|
|
105
|
+
<table className="min-w-full text-left text-[13px]">
|
|
106
|
+
<thead className="bg-black/[0.18]">
|
|
107
|
+
<tr>
|
|
108
|
+
{block.table.columns.map((column) => (
|
|
109
|
+
<th key={column} className="px-3 py-2.5 font-700 text-text-2">{column}</th>
|
|
110
|
+
))}
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody>
|
|
114
|
+
{block.table.rows.map((row, rowIndex) => (
|
|
115
|
+
<tr key={rowIndex} className="border-t border-white/[0.06]">
|
|
116
|
+
{row.map((cell, cellIndex) => (
|
|
117
|
+
<td key={cellIndex} className="px-3 py-2.5 text-text-3/80">{cell == null ? '—' : String(cell)}</td>
|
|
118
|
+
))}
|
|
119
|
+
</tr>
|
|
120
|
+
))}
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
124
|
+
{block.table.caption && <div className="mt-2 text-[11px] text-text-3/60">{block.table.caption}</div>}
|
|
125
|
+
</section>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (block.type === 'code') {
|
|
130
|
+
return (
|
|
131
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
132
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
133
|
+
<pre className="overflow-x-auto rounded-[14px] border border-white/[0.08] bg-black/[0.25] p-4 text-[12px] leading-6 text-text-2">
|
|
134
|
+
<code>{block.code}</code>
|
|
135
|
+
</pre>
|
|
136
|
+
{block.language && <div className={`mt-2 inline-flex rounded-full border px-2 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${theme.chip}`}>{block.language}</div>}
|
|
137
|
+
</section>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (block.type === 'actions') {
|
|
142
|
+
return (
|
|
143
|
+
<section key={`${block.type}-${index}`} className="rounded-[18px] border border-white/[0.08] bg-white/[0.03] px-5 py-4">
|
|
144
|
+
{block.title && <div className={`mb-3 text-[11px] font-700 uppercase tracking-[0.08em] ${theme.accent}`}>{block.title}</div>}
|
|
145
|
+
<div className="flex flex-wrap gap-2">
|
|
146
|
+
{block.items.map((item) => (
|
|
147
|
+
item.href ? (
|
|
148
|
+
<a
|
|
149
|
+
key={item.label}
|
|
150
|
+
href={item.href}
|
|
151
|
+
target="_blank"
|
|
152
|
+
rel="noreferrer"
|
|
153
|
+
className={`inline-flex items-center rounded-[12px] border px-3 py-2 text-[12px] font-700 transition-all hover:brightness-110 ${intentClass(item.intent)}`}
|
|
154
|
+
>
|
|
155
|
+
{item.label}
|
|
156
|
+
</a>
|
|
157
|
+
) : (
|
|
158
|
+
<div key={item.label} className={`inline-flex items-center rounded-[12px] border px-3 py-2 text-[12px] font-700 ${intentClass(item.intent)}`}>
|
|
159
|
+
{item.label}
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
{block.items.some((item) => item.note) && (
|
|
165
|
+
<div className="mt-3 space-y-1">
|
|
166
|
+
{block.items.filter((item) => item.note).map((item) => (
|
|
167
|
+
<div key={`${item.label}-note`} className="text-[11px] text-text-3/60">{item.label}: {item.note}</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</section>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null
|
|
176
|
+
})}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
13
182
|
export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
|
|
14
|
-
const [content, setContent] = useState<
|
|
183
|
+
const [content, setContent] = useState<CanvasContent>(null)
|
|
184
|
+
const [loaded, setLoaded] = useState(false)
|
|
15
185
|
|
|
16
186
|
const loadCanvas = useCallback(async () => {
|
|
17
187
|
try {
|
|
18
|
-
const res = await api<{ content:
|
|
19
|
-
setContent(res.content)
|
|
20
|
-
} catch {
|
|
188
|
+
const res = await api<{ content: CanvasContent }>('GET', `/canvas/${sessionId}`)
|
|
189
|
+
setContent(normalizeCanvasContent(res.content))
|
|
190
|
+
} catch {
|
|
191
|
+
setContent(null)
|
|
192
|
+
} finally {
|
|
193
|
+
setLoaded(true)
|
|
194
|
+
}
|
|
21
195
|
}, [sessionId])
|
|
22
196
|
|
|
23
|
-
useEffect(() => { loadCanvas() }, [loadCanvas])
|
|
197
|
+
useEffect(() => { loadCanvas() }, [loadCanvas])
|
|
24
198
|
useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
|
|
25
199
|
|
|
26
|
-
|
|
27
|
-
<div className="flex
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
200
|
+
const header = (
|
|
201
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
202
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
203
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
|
|
204
|
+
</svg>
|
|
205
|
+
<span className="text-[13px] font-600 text-text flex-1 truncate">
|
|
206
|
+
Canvas{agentName ? ` — ${agentName}` : ''}
|
|
207
|
+
</span>
|
|
208
|
+
<button
|
|
209
|
+
onClick={loadCanvas}
|
|
210
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
211
|
+
title="Refresh"
|
|
212
|
+
>
|
|
213
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
214
|
+
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
31
215
|
</svg>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
43
|
-
</svg>
|
|
44
|
-
</button>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="flex-1 flex items-center justify-center">
|
|
47
|
-
<div className="text-center">
|
|
48
|
-
<div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
|
|
49
|
-
<span className="text-[13px] text-text-3">Loading canvas...</span>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
216
|
+
</button>
|
|
217
|
+
<button
|
|
218
|
+
onClick={onClose}
|
|
219
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
220
|
+
title="Close canvas"
|
|
221
|
+
>
|
|
222
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
223
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
52
226
|
</div>
|
|
53
227
|
)
|
|
54
228
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</
|
|
65
|
-
<button
|
|
66
|
-
onClick={loadCanvas}
|
|
67
|
-
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
68
|
-
title="Refresh"
|
|
69
|
-
>
|
|
70
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
71
|
-
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
72
|
-
</svg>
|
|
73
|
-
</button>
|
|
74
|
-
<button
|
|
75
|
-
onClick={onClose}
|
|
76
|
-
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
77
|
-
title="Close canvas"
|
|
78
|
-
>
|
|
79
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
80
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
81
|
-
</svg>
|
|
82
|
-
</button>
|
|
229
|
+
if (!loaded) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
232
|
+
{header}
|
|
233
|
+
<div className="flex-1 flex items-center justify-center">
|
|
234
|
+
<div className="text-center">
|
|
235
|
+
<div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
|
|
236
|
+
<span className="text-[13px] text-text-3">Loading canvas...</span>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
83
239
|
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
84
242
|
|
|
85
|
-
|
|
243
|
+
return (
|
|
244
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
245
|
+
{header}
|
|
86
246
|
<div className="flex-1 overflow-hidden">
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
247
|
+
{!content ? (
|
|
248
|
+
<div className="h-full flex items-center justify-center text-center px-6">
|
|
249
|
+
<div>
|
|
250
|
+
<div className="text-[14px] font-600 text-text-2">No canvas content yet</div>
|
|
251
|
+
<p className="mt-1 text-[12px] text-text-3/60">Agents can present HTML or structured documents here.</p>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
) : typeof content === 'string' ? (
|
|
255
|
+
<iframe
|
|
256
|
+
sandbox="allow-scripts allow-same-origin"
|
|
257
|
+
srcDoc={content}
|
|
258
|
+
className="w-full h-full border-none bg-white"
|
|
259
|
+
title="Agent Canvas"
|
|
260
|
+
/>
|
|
261
|
+
) : (
|
|
262
|
+
<StructuredCanvasView document={content} />
|
|
263
|
+
)}
|
|
93
264
|
</div>
|
|
94
265
|
</div>
|
|
95
266
|
)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react'
|
|
3
4
|
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
4
5
|
import type { Session } from '@/types'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
8
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
9
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
8
10
|
import { ConnectorPlatformBadge, getSessionConnector } from '@/components/shared/connector-platform-icon'
|
|
9
11
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
10
12
|
import { toast } from 'sonner'
|
|
@@ -44,18 +46,26 @@ export function ChatCard({ session, active, onClick }: Props) {
|
|
|
44
46
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
45
47
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
46
48
|
const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
|
|
49
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
50
|
+
const [deleting, setDeleting] = useState(false)
|
|
47
51
|
const isTyping = streamingSessionId === session.id
|
|
48
52
|
|
|
49
|
-
const
|
|
53
|
+
const handleDeleteClick = (e: React.MouseEvent) => {
|
|
50
54
|
e.stopPropagation()
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
setConfirmDelete(true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleDelete = async () => {
|
|
59
|
+
setDeleting(true)
|
|
53
60
|
try {
|
|
54
61
|
await api('DELETE', `/chats/${session.id}`)
|
|
55
62
|
removeSession(session.id)
|
|
56
63
|
toast.success('Session deleted')
|
|
57
64
|
} catch (err: unknown) {
|
|
58
65
|
toast.error(err instanceof Error ? err.message : 'Failed to delete session')
|
|
66
|
+
} finally {
|
|
67
|
+
setDeleting(false)
|
|
68
|
+
setConfirmDelete(false)
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -84,14 +94,15 @@ export function ChatCard({ session, active, onClick }: Props) {
|
|
|
84
94
|
&& agent?.heartbeatEnabled !== false
|
|
85
95
|
|
|
86
96
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
<>
|
|
98
|
+
<div
|
|
99
|
+
onClick={onClick}
|
|
100
|
+
className={`group/card relative py-3.5 px-4 cursor-pointer rounded-[14px]
|
|
101
|
+
transition-all duration-200 active:scale-[0.98]
|
|
102
|
+
${active
|
|
103
|
+
? 'bg-accent-soft border border-accent-bright/10'
|
|
104
|
+
: 'bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03]'}`}
|
|
105
|
+
>
|
|
95
106
|
{active && (
|
|
96
107
|
<div className="absolute left-0 top-3.5 bottom-3.5 w-[2.5px] rounded-full bg-accent-bright" />
|
|
97
108
|
)}
|
|
@@ -134,7 +145,7 @@ export function ChatCard({ session, active, onClick }: Props) {
|
|
|
134
145
|
{timeAgo(session.lastActiveAt)}
|
|
135
146
|
</span>
|
|
136
147
|
<button
|
|
137
|
-
onClick={
|
|
148
|
+
onClick={handleDeleteClick}
|
|
138
149
|
className="shrink-0 opacity-0 group-hover/card:opacity-100 transition-opacity duration-150
|
|
139
150
|
text-text-3 hover:text-red-400 p-0.5 -mr-1 cursor-pointer bg-transparent border-none"
|
|
140
151
|
title="Delete chat"
|
|
@@ -163,6 +174,18 @@ export function ChatCard({ session, active, onClick }: Props) {
|
|
|
163
174
|
) : (
|
|
164
175
|
<div className="text-[13px] text-text-2/50 truncate mt-1 leading-relaxed">{preview}</div>
|
|
165
176
|
)}
|
|
166
|
-
|
|
177
|
+
</div>
|
|
178
|
+
<ConfirmDialog
|
|
179
|
+
open={confirmDelete}
|
|
180
|
+
title="Delete Chat?"
|
|
181
|
+
message={`Delete chat session "${session.name}"?`}
|
|
182
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
183
|
+
confirmDisabled={deleting}
|
|
184
|
+
cancelDisabled={deleting}
|
|
185
|
+
danger
|
|
186
|
+
onConfirm={() => { void handleDelete() }}
|
|
187
|
+
onCancel={() => { if (!deleting) setConfirmDelete(false) }}
|
|
188
|
+
/>
|
|
189
|
+
</>
|
|
167
190
|
)
|
|
168
191
|
}
|
|
@@ -10,8 +10,8 @@ import { ChatToolToggles } from './chat-tool-toggles'
|
|
|
10
10
|
import { api } from '@/lib/api-client'
|
|
11
11
|
import {
|
|
12
12
|
ConnectorPlatformIcon,
|
|
13
|
-
CONNECTOR_PLATFORM_META,
|
|
14
13
|
getSessionConnector,
|
|
14
|
+
resolveConnectorPlatformMeta,
|
|
15
15
|
} from '@/components/shared/connector-platform-icon'
|
|
16
16
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
17
17
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
@@ -34,6 +34,23 @@ function Tip({ label, children, side = 'bottom' }: { label: string; children: Re
|
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function getAgentWalletIds(agent: { walletIds?: string[]; walletId?: string | null } | null | undefined): string[] {
|
|
38
|
+
const ids = Array.isArray(agent?.walletIds)
|
|
39
|
+
? agent.walletIds.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
40
|
+
: []
|
|
41
|
+
const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
|
|
42
|
+
? [agent.walletId.trim()]
|
|
43
|
+
: []
|
|
44
|
+
return [...new Set([...ids, ...legacy])]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getAgentActiveWalletId(agent: { activeWalletId?: string | null; walletIds?: string[]; walletId?: string | null } | null | undefined): string | null {
|
|
48
|
+
const walletIds = getAgentWalletIds(agent)
|
|
49
|
+
if (typeof agent?.activeWalletId === 'string' && walletIds.includes(agent.activeWalletId)) return agent.activeWalletId
|
|
50
|
+
if (typeof agent?.walletId === 'string' && walletIds.includes(agent.walletId)) return agent.walletId
|
|
51
|
+
return walletIds[0] || null
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
function HeaderChip({
|
|
38
55
|
children,
|
|
39
56
|
title,
|
|
@@ -129,7 +146,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
129
146
|
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
130
147
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
131
148
|
const connector = getSessionConnector(session, connectors)
|
|
132
|
-
const connectorMeta = connector ?
|
|
149
|
+
const connectorMeta = connector ? resolveConnectorPlatformMeta(connector.platform) : null
|
|
133
150
|
const connectorPresence = connector?.presence
|
|
134
151
|
const providers = useAppStore((s) => s.providers)
|
|
135
152
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
@@ -152,8 +169,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
152
169
|
const renameInputRef = useRef<HTMLInputElement>(null)
|
|
153
170
|
const renameContainerRef = useRef<HTMLSpanElement>(null)
|
|
154
171
|
const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
|
|
155
|
-
const [walletBalance, setWalletBalance] = useState<number | null>(null)
|
|
172
|
+
const [walletBalance, setWalletBalance] = useState<{ formatted: string; symbol: string; assets?: number } | null>(null)
|
|
156
173
|
const [headerWidgets, setHeaderWidgets] = useState<Array<{ id: string; label: string; icon?: string }>>([])
|
|
174
|
+
const agentWalletIds = useMemo(() => getAgentWalletIds(agent), [agent])
|
|
175
|
+
const activeWalletId = useMemo(() => getAgentActiveWalletId(agent), [agent])
|
|
157
176
|
|
|
158
177
|
useEffect(() => {
|
|
159
178
|
api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
|
|
@@ -162,17 +181,25 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
162
181
|
}, [session.id])
|
|
163
182
|
|
|
164
183
|
const fetchWalletBalance = useCallback(async () => {
|
|
165
|
-
if (!
|
|
184
|
+
if (!activeWalletId) {
|
|
166
185
|
setWalletBalance(null)
|
|
167
186
|
return
|
|
168
187
|
}
|
|
169
188
|
try {
|
|
170
|
-
const data = await api<{
|
|
171
|
-
|
|
189
|
+
const data = await api<{ balanceFormatted?: string; balanceSymbol?: string; portfolioSummary?: { nonZeroAssets?: number } }>('GET', `/wallets/${activeWalletId}`)
|
|
190
|
+
if (data.balanceFormatted && data.balanceSymbol) {
|
|
191
|
+
setWalletBalance({
|
|
192
|
+
formatted: data.balanceFormatted,
|
|
193
|
+
symbol: data.balanceSymbol,
|
|
194
|
+
assets: typeof data.portfolioSummary?.nonZeroAssets === 'number' ? data.portfolioSummary.nonZeroAssets : undefined,
|
|
195
|
+
})
|
|
196
|
+
} else {
|
|
197
|
+
setWalletBalance(null)
|
|
198
|
+
}
|
|
172
199
|
} catch {
|
|
173
200
|
setWalletBalance(null)
|
|
174
201
|
}
|
|
175
|
-
}, [
|
|
202
|
+
}, [activeWalletId])
|
|
176
203
|
|
|
177
204
|
useEffect(() => {
|
|
178
205
|
void fetchWalletBalance()
|
|
@@ -237,17 +264,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
237
264
|
title: 'Open wallets',
|
|
238
265
|
}
|
|
239
266
|
}
|
|
240
|
-
if (
|
|
267
|
+
if (agentWalletIds.length === 0) {
|
|
241
268
|
return {
|
|
242
269
|
label: 'Create wallet',
|
|
243
270
|
title: 'Create wallet',
|
|
244
271
|
}
|
|
245
272
|
}
|
|
246
273
|
return {
|
|
247
|
-
label:
|
|
248
|
-
|
|
274
|
+
label: agentWalletIds.length > 1
|
|
275
|
+
? (walletBalance ? `${walletBalance.formatted} ${walletBalance.symbol}${walletBalance.assets && walletBalance.assets > 1 ? ` +${walletBalance.assets - 1}` : ''} / ${agentWalletIds.length}` : `${agentWalletIds.length} wallets`)
|
|
276
|
+
: (walletBalance ? `${walletBalance.formatted} ${walletBalance.symbol}${walletBalance.assets && walletBalance.assets > 1 ? ` +${walletBalance.assets - 1}` : ''}` : 'Wallet'),
|
|
277
|
+
title: agentWalletIds.length > 1 ? 'View wallets' : 'View wallet',
|
|
249
278
|
}
|
|
250
|
-
}, [agent?.id,
|
|
279
|
+
}, [agent?.id, agentWalletIds, walletBalance])
|
|
251
280
|
|
|
252
281
|
const handleHeaderWidgetClick = (widgetId: string) => {
|
|
253
282
|
if (widgetId === 'wallet-status') {
|
|
@@ -389,17 +418,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
389
418
|
setHeartbeatSaving(true)
|
|
390
419
|
try {
|
|
391
420
|
if (session.agentId) {
|
|
392
|
-
// Save
|
|
421
|
+
// Save the cadence without implicitly toggling heartbeat on.
|
|
393
422
|
await api('PUT', `/agents/${session.agentId}`, {
|
|
394
423
|
heartbeatInterval: formatDuration(sec),
|
|
395
424
|
heartbeatIntervalSec: sec,
|
|
396
|
-
heartbeatEnabled: true,
|
|
397
425
|
})
|
|
398
426
|
// Clear stale session-level overrides
|
|
399
427
|
await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
|
|
400
428
|
await Promise.all([loadAgents(), loadSessions()])
|
|
401
429
|
} else {
|
|
402
|
-
await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec
|
|
430
|
+
await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec })
|
|
403
431
|
await loadSessions()
|
|
404
432
|
}
|
|
405
433
|
} finally {
|
|
@@ -960,7 +988,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
960
988
|
</button>
|
|
961
989
|
{Array.from(connectorSources.entries()).map(([cid, info]) => {
|
|
962
990
|
const active = connectorFilter === cid
|
|
963
|
-
const meta =
|
|
991
|
+
const meta = resolveConnectorPlatformMeta(info.platform)
|
|
964
992
|
return (
|
|
965
993
|
<button
|
|
966
994
|
key={cid}
|
|
@@ -969,7 +997,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
969
997
|
active ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
|
|
970
998
|
}`}
|
|
971
999
|
>
|
|
972
|
-
<ConnectorPlatformIcon platform={info.platform
|
|
1000
|
+
<ConnectorPlatformIcon platform={info.platform} size={12} />
|
|
973
1001
|
{info.connectorName || meta?.label || info.platform}
|
|
974
1002
|
</button>
|
|
975
1003
|
)
|