@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
|
@@ -233,9 +233,9 @@ export function GatewaySheet() {
|
|
|
233
233
|
params.set('endpoint', endpoint.trim() || 'http://localhost:18789')
|
|
234
234
|
if (credentialId) params.set('credentialId', credentialId)
|
|
235
235
|
if (tokenDraft.trim()) params.set('token', tokenDraft.trim())
|
|
236
|
-
const result = await api<{ ok: boolean; models: string[]; error?: string; hint?: string }>('GET', `/providers/openclaw/health?${params.toString()}`)
|
|
236
|
+
const result = await api<{ ok: boolean; models: string[]; message?: string; error?: string; hint?: string }>('GET', `/providers/openclaw/health?${params.toString()}`)
|
|
237
237
|
if (result.ok) {
|
|
238
|
-
setCheckMessage(`Connected. ${result.models?.length ? `${result.models.length} model${result.models.length === 1 ? '' : 's'} visible.` : 'Gateway responded normally.'}`)
|
|
238
|
+
setCheckMessage(result.message || `Connected. ${result.models?.length ? `${result.models.length} model${result.models.length === 1 ? '' : 's'} visible.` : 'Gateway responded normally.'}`)
|
|
239
239
|
} else {
|
|
240
240
|
setCheckMessage(result.error || result.hint || 'Gateway health check failed.')
|
|
241
241
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
import { toast } from 'sonner'
|
|
7
8
|
|
|
@@ -62,6 +63,8 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
62
63
|
const [invokeResult, setInvokeResult] = useState<McpInvokeResult | null>(null)
|
|
63
64
|
const [conformanceByServer, setConformanceByServer] = useState<Record<string, McpConformanceResult>>({})
|
|
64
65
|
const [conformanceLoading, setConformanceLoading] = useState<Record<string, boolean>>({})
|
|
66
|
+
const [confirmDelete, setConfirmDelete] = useState<{ id: string; name: string } | null>(null)
|
|
67
|
+
const [deletingId, setDeletingId] = useState<string | null>(null)
|
|
65
68
|
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])
|
|
66
69
|
|
|
67
70
|
useEffect(() => {
|
|
@@ -109,17 +112,24 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
109
112
|
setMcpServerSheetOpen(true)
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
const handleDelete =
|
|
115
|
+
const handleDelete = (e: React.MouseEvent, id: string) => {
|
|
113
116
|
e.stopPropagation()
|
|
114
117
|
const server = mcpServers[id]
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
setConfirmDelete({ id, name: server?.name || id })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const handleDeleteConfirm = async () => {
|
|
122
|
+
if (!confirmDelete) return
|
|
123
|
+
setDeletingId(confirmDelete.id)
|
|
117
124
|
try {
|
|
118
|
-
await api('DELETE', `/mcp-servers/${id}`)
|
|
125
|
+
await api('DELETE', `/mcp-servers/${confirmDelete.id}`)
|
|
119
126
|
toast.success('MCP server deleted')
|
|
120
127
|
await loadMcpServers()
|
|
128
|
+
setConfirmDelete(null)
|
|
121
129
|
} catch (err: unknown) {
|
|
122
130
|
toast.error(err instanceof Error ? err.message : 'Failed to delete server')
|
|
131
|
+
} finally {
|
|
132
|
+
setDeletingId(null)
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
|
|
@@ -280,7 +290,7 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
280
290
|
) : inspectorError ? (
|
|
281
291
|
<p className="text-[12px] text-red-300">{inspectorError}</p>
|
|
282
292
|
) : (
|
|
283
|
-
|
|
293
|
+
<div className="space-y-3">
|
|
284
294
|
{activeConformance && (
|
|
285
295
|
<div className={`rounded-[10px] border p-3 ${activeConformance.ok ? 'border-emerald-400/20 bg-emerald-500/[0.06]' : 'border-amber-400/20 bg-amber-500/[0.06]'}`}>
|
|
286
296
|
<p className={`text-[12px] font-600 mb-1 ${activeConformance.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
|
|
@@ -441,6 +451,17 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
441
451
|
</div>
|
|
442
452
|
</>
|
|
443
453
|
)}
|
|
454
|
+
<ConfirmDialog
|
|
455
|
+
open={!!confirmDelete}
|
|
456
|
+
title="Delete MCP Server?"
|
|
457
|
+
message={confirmDelete ? `Delete "${confirmDelete.name}"? This will remove the MCP server from the app.` : 'Delete this MCP server?'}
|
|
458
|
+
confirmLabel={deletingId ? 'Deleting...' : 'Delete'}
|
|
459
|
+
confirmDisabled={!!deletingId}
|
|
460
|
+
cancelDisabled={!!deletingId}
|
|
461
|
+
danger
|
|
462
|
+
onConfirm={() => { void handleDeleteConfirm() }}
|
|
463
|
+
onCancel={() => { if (!deletingId) setConfirmDelete(null) }}
|
|
464
|
+
/>
|
|
444
465
|
</div>
|
|
445
466
|
)
|
|
446
467
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { 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 { toast } from 'sonner'
|
|
8
9
|
import type { McpServerConfig, McpTransport } from '@/types'
|
|
@@ -25,6 +26,8 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
25
26
|
)
|
|
26
27
|
const [testing, setTesting] = useState(false)
|
|
27
28
|
const [testResult, setTestResult] = useState<{ ok: boolean; tools?: string[]; error?: string } | null>(null)
|
|
29
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
30
|
+
const [deleting, setDeleting] = useState(false)
|
|
28
31
|
|
|
29
32
|
const parseEnv = (text: string): Record<string, string> | undefined => {
|
|
30
33
|
if (!text.trim()) return undefined
|
|
@@ -76,14 +79,17 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
76
79
|
|
|
77
80
|
const handleDelete = async () => {
|
|
78
81
|
if (!editing) return
|
|
79
|
-
|
|
82
|
+
setDeleting(true)
|
|
80
83
|
try {
|
|
81
84
|
await api('DELETE', `/mcp-servers/${editing.id}`)
|
|
82
85
|
toast.success('MCP server deleted')
|
|
83
86
|
await loadMcpServers()
|
|
87
|
+
setConfirmDelete(false)
|
|
84
88
|
onClose()
|
|
85
89
|
} catch (err: unknown) {
|
|
86
90
|
toast.error(err instanceof Error ? err.message : 'Failed to delete server')
|
|
91
|
+
} finally {
|
|
92
|
+
setDeleting(false)
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
@@ -216,7 +222,7 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
216
222
|
|
|
217
223
|
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
218
224
|
{editing && (
|
|
219
|
-
<button onClick={
|
|
225
|
+
<button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
220
226
|
Delete
|
|
221
227
|
</button>
|
|
222
228
|
)}
|
|
@@ -227,6 +233,17 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
227
233
|
{editing ? 'Save' : 'Create'}
|
|
228
234
|
</button>
|
|
229
235
|
</div>
|
|
236
|
+
<ConfirmDialog
|
|
237
|
+
open={confirmDelete}
|
|
238
|
+
title="Delete MCP Server?"
|
|
239
|
+
message={editing ? `Delete "${editing.name}"? This will remove the MCP server from the app.` : 'Delete this MCP server?'}
|
|
240
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
241
|
+
confirmDisabled={deleting}
|
|
242
|
+
cancelDisabled={deleting}
|
|
243
|
+
danger
|
|
244
|
+
onConfirm={() => { void handleDelete() }}
|
|
245
|
+
onCancel={() => { if (!deleting) setConfirmDelete(false) }}
|
|
246
|
+
/>
|
|
230
247
|
</>
|
|
231
248
|
)
|
|
232
249
|
}
|
|
@@ -10,6 +10,9 @@ type UseCaseTemplate = 'local-dev' | 'single-vps' | 'private-tailnet' | 'browser
|
|
|
10
10
|
type ExposurePreset = 'private-lan' | 'tailscale' | 'caddy' | 'nginx' | 'ssh-tunnel'
|
|
11
11
|
|
|
12
12
|
interface LocalDeployStatus {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
isPrimary: boolean
|
|
13
16
|
running: boolean
|
|
14
17
|
processId: string | null
|
|
15
18
|
pid: number | null
|
|
@@ -18,6 +21,8 @@ interface LocalDeployStatus {
|
|
|
18
21
|
wsUrl: string
|
|
19
22
|
token: string | null
|
|
20
23
|
startedAt: number | null
|
|
24
|
+
createdAt: number
|
|
25
|
+
updatedAt: number
|
|
21
26
|
tail: string
|
|
22
27
|
lastError: string | null
|
|
23
28
|
launchCommand: string
|
|
@@ -25,12 +30,17 @@ interface LocalDeployStatus {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
interface RemoteDeployStatus {
|
|
33
|
+
id: string
|
|
34
|
+
name: string
|
|
35
|
+
isPrimary: boolean
|
|
28
36
|
active: boolean
|
|
29
37
|
processId: string | null
|
|
30
38
|
pid: number | null
|
|
31
39
|
action: string | null
|
|
32
40
|
target: string | null
|
|
33
41
|
startedAt: number | null
|
|
42
|
+
createdAt: number
|
|
43
|
+
updatedAt: number
|
|
34
44
|
status: 'idle' | 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
|
|
35
45
|
exitCode: number | null
|
|
36
46
|
tail: string
|
|
@@ -63,16 +73,24 @@ interface DeployBundle {
|
|
|
63
73
|
|
|
64
74
|
interface DeployStatusResponse {
|
|
65
75
|
local: LocalDeployStatus
|
|
66
|
-
|
|
76
|
+
locals?: LocalDeployStatus[]
|
|
77
|
+
localPrimaryId?: string | null
|
|
78
|
+
remote?: RemoteDeployStatus | null
|
|
79
|
+
remotes?: RemoteDeployStatus[]
|
|
80
|
+
remotePrimaryId?: string | null
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
interface DeployActionResponse {
|
|
70
84
|
ok: boolean
|
|
71
85
|
local?: LocalDeployStatus
|
|
86
|
+
locals?: LocalDeployStatus[]
|
|
87
|
+
localPrimaryId?: string | null
|
|
72
88
|
token?: string
|
|
73
89
|
bundle?: DeployBundle
|
|
74
90
|
processId?: string | null
|
|
75
91
|
remote?: RemoteDeployStatus
|
|
92
|
+
remotes?: RemoteDeployStatus[]
|
|
93
|
+
remotePrimaryId?: string | null
|
|
76
94
|
summary?: string
|
|
77
95
|
commandPreview?: string
|
|
78
96
|
verify?: {
|
|
@@ -81,6 +99,7 @@ interface DeployActionResponse {
|
|
|
81
99
|
wsUrl: string
|
|
82
100
|
authProvided: boolean
|
|
83
101
|
models: string[]
|
|
102
|
+
message?: string
|
|
84
103
|
error?: string
|
|
85
104
|
hint?: string
|
|
86
105
|
}
|
|
@@ -239,6 +258,12 @@ function inferRemoteTarget(value: string | null | undefined): string {
|
|
|
239
258
|
return base.replace(/\/+$/, '')
|
|
240
259
|
}
|
|
241
260
|
|
|
261
|
+
function inferRemoteHost(value: string | null | undefined): string {
|
|
262
|
+
const parsed = parseMaybeUrl(value)
|
|
263
|
+
if (!parsed || isLocalEndpoint(value)) return ''
|
|
264
|
+
return parsed.hostname
|
|
265
|
+
}
|
|
266
|
+
|
|
242
267
|
function badgeTone(active: boolean): string {
|
|
243
268
|
return active
|
|
244
269
|
? 'border-accent-bright/30 bg-accent-bright/10 text-accent-bright'
|
|
@@ -259,7 +284,9 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
259
284
|
|
|
260
285
|
const [activeTab, setActiveTab] = useState<'local' | 'remote'>('local')
|
|
261
286
|
const [localStatus, setLocalStatus] = useState<LocalDeployStatus | null>(null)
|
|
287
|
+
const [localStatuses, setLocalStatuses] = useState<LocalDeployStatus[]>([])
|
|
262
288
|
const [remoteStatus, setRemoteStatus] = useState<RemoteDeployStatus | null>(null)
|
|
289
|
+
const [remoteStatuses, setRemoteStatuses] = useState<RemoteDeployStatus[]>([])
|
|
263
290
|
const [localPort, setLocalPort] = useState(() => inferPort(endpoint))
|
|
264
291
|
const [deployToken, setDeployToken] = useState(token || '')
|
|
265
292
|
const [remoteTarget, setRemoteTarget] = useState(() => inferRemoteTarget(endpoint))
|
|
@@ -270,7 +297,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
270
297
|
const [remoteProvider, setRemoteProvider] = useState<RemoteProvider>('hetzner')
|
|
271
298
|
const [useCase, setUseCase] = useState<UseCaseTemplate>(() => deployment?.useCase || 'single-vps')
|
|
272
299
|
const [exposure, setExposure] = useState<ExposurePreset>(() => deployment?.exposure || 'caddy')
|
|
273
|
-
const [sshHost, setSshHost] = useState(() => deployment?.sshHost ||
|
|
300
|
+
const [sshHost, setSshHost] = useState(() => deployment?.sshHost || inferRemoteHost(endpoint))
|
|
274
301
|
const [sshUser, setSshUser] = useState(() => deployment?.sshUser || 'root')
|
|
275
302
|
const [sshPort, setSshPort] = useState(() => deployment?.sshPort || 22)
|
|
276
303
|
const [sshKeyPath, setSshKeyPath] = useState(() => deployment?.sshKeyPath || '')
|
|
@@ -295,7 +322,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
295
322
|
setActiveTab('local')
|
|
296
323
|
} else if (endpoint && inferRemoteTarget(endpoint)) {
|
|
297
324
|
setRemoteTarget(inferRemoteTarget(endpoint))
|
|
298
|
-
setSshHost((current) => current ||
|
|
325
|
+
setSshHost((current) => current || inferRemoteHost(endpoint))
|
|
299
326
|
setActiveTab('remote')
|
|
300
327
|
}
|
|
301
328
|
}, [endpoint])
|
|
@@ -318,7 +345,12 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
318
345
|
.then((result) => {
|
|
319
346
|
if (!cancelled) {
|
|
320
347
|
setLocalStatus(result.local)
|
|
321
|
-
|
|
348
|
+
setLocalStatuses(Array.isArray(result.locals) ? result.locals : [])
|
|
349
|
+
const nextRemotes = Array.isArray(result.remotes)
|
|
350
|
+
? result.remotes
|
|
351
|
+
: (result.remote ? [result.remote] : [])
|
|
352
|
+
setRemoteStatuses(nextRemotes)
|
|
353
|
+
setRemoteStatus(nextRemotes.find((item) => item.isPrimary) || nextRemotes[0] || null)
|
|
322
354
|
if (result.local.token) {
|
|
323
355
|
setDeployToken((current) => current || result.local.token || '')
|
|
324
356
|
}
|
|
@@ -330,23 +362,47 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
330
362
|
}
|
|
331
363
|
}, [])
|
|
332
364
|
|
|
365
|
+
const hasActiveRemote = useMemo(
|
|
366
|
+
() => remoteStatuses.some((status) => status.active) || !!remoteStatus?.active,
|
|
367
|
+
[remoteStatus?.active, remoteStatuses],
|
|
368
|
+
)
|
|
369
|
+
|
|
333
370
|
useEffect(() => {
|
|
334
|
-
if (!
|
|
371
|
+
if (!hasActiveRemote) return
|
|
335
372
|
const timer = window.setInterval(() => {
|
|
336
373
|
api<DeployStatusResponse>('GET', '/openclaw/deploy')
|
|
337
374
|
.then((result) => {
|
|
338
375
|
setLocalStatus(result.local)
|
|
339
|
-
|
|
376
|
+
setLocalStatuses(Array.isArray(result.locals) ? result.locals : [])
|
|
377
|
+
const nextRemotes = Array.isArray(result.remotes)
|
|
378
|
+
? result.remotes
|
|
379
|
+
: (result.remote ? [result.remote] : [])
|
|
380
|
+
setRemoteStatuses(nextRemotes)
|
|
381
|
+
setRemoteStatus((current) => {
|
|
382
|
+
const currentId = current?.id || ''
|
|
383
|
+
return nextRemotes.find((item) => item.id === currentId)
|
|
384
|
+
|| nextRemotes.find((item) => item.isPrimary)
|
|
385
|
+
|| nextRemotes[0]
|
|
386
|
+
|| null
|
|
387
|
+
})
|
|
340
388
|
})
|
|
341
389
|
.catch(() => {})
|
|
342
390
|
}, 2500)
|
|
343
391
|
return () => window.clearInterval(timer)
|
|
344
|
-
}, [
|
|
392
|
+
}, [hasActiveRemote])
|
|
345
393
|
|
|
346
394
|
const selectedFile = useMemo(() => {
|
|
347
395
|
if (!bundle) return null
|
|
348
396
|
return bundle.files.find((file) => file.name === bundleFile) || bundle.files[0] || null
|
|
349
397
|
}, [bundle, bundleFile])
|
|
398
|
+
const visibleLocalStatuses = useMemo(
|
|
399
|
+
() => localStatuses,
|
|
400
|
+
[localStatuses],
|
|
401
|
+
)
|
|
402
|
+
const visibleRemoteStatuses = useMemo(
|
|
403
|
+
() => remoteStatuses,
|
|
404
|
+
[remoteStatuses],
|
|
405
|
+
)
|
|
350
406
|
const localLaunchCommand = useMemo(() => {
|
|
351
407
|
const typedToken = deployToken.trim()
|
|
352
408
|
if (typedToken) return buildLocalRunCommand(localPort, typedToken)
|
|
@@ -368,6 +424,35 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
368
424
|
}, 2200)
|
|
369
425
|
}
|
|
370
426
|
|
|
427
|
+
const syncLocalResponse = (result: Pick<DeployActionResponse, 'local' | 'locals'>) => {
|
|
428
|
+
if (result.local) setLocalStatus(result.local)
|
|
429
|
+
if (Array.isArray(result.locals)) {
|
|
430
|
+
setLocalStatuses(result.locals)
|
|
431
|
+
} else if (result.local) {
|
|
432
|
+
setLocalStatuses([result.local])
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const syncRemoteResponse = (result: Pick<DeployActionResponse, 'remote' | 'remotes'>) => {
|
|
437
|
+
const remotes = Array.isArray(result.remotes) ? result.remotes : null
|
|
438
|
+
if (remotes) {
|
|
439
|
+
setRemoteStatuses(remotes)
|
|
440
|
+
setRemoteStatus((current) => {
|
|
441
|
+
const currentId = current?.id || ''
|
|
442
|
+
return (result.remote && remotes.find((item) => item.id === result.remote?.id))
|
|
443
|
+
|| remotes.find((item) => item.id === currentId)
|
|
444
|
+
|| result.remote
|
|
445
|
+
|| remotes[0]
|
|
446
|
+
|| null
|
|
447
|
+
})
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (result.remote) {
|
|
451
|
+
setRemoteStatus(result.remote)
|
|
452
|
+
setRemoteStatuses([result.remote])
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
371
456
|
const onCopied = async (key: string, value: string) => {
|
|
372
457
|
const ok = await copyTextToClipboard(value)
|
|
373
458
|
if (!ok) return
|
|
@@ -381,6 +466,13 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
381
466
|
await Promise.resolve(onApply?.(patch))
|
|
382
467
|
}
|
|
383
468
|
|
|
469
|
+
const handleSelectRemote = (status: RemoteDeployStatus) => {
|
|
470
|
+
setRemoteStatus(status)
|
|
471
|
+
if (status.target) {
|
|
472
|
+
setSshHost(status.target)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
384
476
|
const buildRemoteDeploymentPatch = (overrides?: Partial<NonNullable<ApplyPatch['deployment']>>): NonNullable<ApplyPatch['deployment']> => ({
|
|
385
477
|
method: overrides?.method || (remoteTemplate === 'docker' ? 'bundle' : 'bundle'),
|
|
386
478
|
provider: overrides?.provider || (remoteTemplate === 'docker' ? remoteProvider : remoteTemplate),
|
|
@@ -413,7 +505,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
413
505
|
token: deployToken.trim() || undefined,
|
|
414
506
|
})
|
|
415
507
|
if (!result.ok || !result.local) throw new Error(result.error || 'Local OpenClaw deploy failed.')
|
|
416
|
-
|
|
508
|
+
syncLocalResponse(result)
|
|
417
509
|
if (result.token) setDeployToken(result.token)
|
|
418
510
|
const verify = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
419
511
|
action: 'verify',
|
|
@@ -422,7 +514,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
422
514
|
}).catch(() => ({ ok: false } as DeployActionResponse))
|
|
423
515
|
if (verify.verify) {
|
|
424
516
|
setVerifySummary(verify.verify.ok
|
|
425
|
-
? `Verified ${verify.verify.endpoint} with ${verify.verify.models.length} model${verify.verify.models.length === 1 ? '' : 's'}.`
|
|
517
|
+
? (verify.verify.message || `Verified ${verify.verify.endpoint} with ${verify.verify.models.length} model${verify.verify.models.length === 1 ? '' : 's'}.`)
|
|
426
518
|
: (verify.verify.error || verify.verify.hint || 'Verification failed.'))
|
|
427
519
|
}
|
|
428
520
|
await applyDeploymentPatch({
|
|
@@ -441,7 +533,9 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
441
533
|
lastVerifiedAt: verify.verify ? Date.now() : null,
|
|
442
534
|
lastVerifiedOk: verify.verify?.ok ?? null,
|
|
443
535
|
lastVerifiedMessage: verify.verify
|
|
444
|
-
? (verify.verify.
|
|
536
|
+
? (verify.verify.ok
|
|
537
|
+
? (verify.verify.message || 'Verified successfully.')
|
|
538
|
+
: (verify.verify.error || verify.verify.hint || 'Verification failed.'))
|
|
445
539
|
: null,
|
|
446
540
|
},
|
|
447
541
|
})
|
|
@@ -457,9 +551,9 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
457
551
|
setLoading('stopping-local')
|
|
458
552
|
setError('')
|
|
459
553
|
try {
|
|
460
|
-
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', { action: 'stop-local' })
|
|
554
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', { action: 'stop-local', localId: localStatus?.id || undefined })
|
|
461
555
|
if (!result.ok || !result.local) throw new Error(result.error || 'Failed to stop local OpenClaw.')
|
|
462
|
-
|
|
556
|
+
syncLocalResponse(result)
|
|
463
557
|
showMessage('Stopped managed local OpenClaw runtime.')
|
|
464
558
|
} catch (err: unknown) {
|
|
465
559
|
setError(err instanceof Error ? err.message : 'Failed to stop local OpenClaw.')
|
|
@@ -474,11 +568,12 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
474
568
|
try {
|
|
475
569
|
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
476
570
|
action: 'restart-local',
|
|
571
|
+
localId: localStatus?.id || undefined,
|
|
477
572
|
port: localPort,
|
|
478
573
|
token: deployToken.trim() || undefined,
|
|
479
574
|
})
|
|
480
575
|
if (!result.ok || !result.local) throw new Error(result.error || 'Failed to restart local OpenClaw.')
|
|
481
|
-
|
|
576
|
+
syncLocalResponse(result)
|
|
482
577
|
if (result.token) setDeployToken(result.token)
|
|
483
578
|
await applyDeploymentPatch({
|
|
484
579
|
endpoint: result.local.endpoint,
|
|
@@ -503,6 +598,45 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
503
598
|
}
|
|
504
599
|
}
|
|
505
600
|
|
|
601
|
+
const handleRestartSpecificLocal = async (status: LocalDeployStatus) => {
|
|
602
|
+
setLoading('restarting-local')
|
|
603
|
+
setError('')
|
|
604
|
+
try {
|
|
605
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
606
|
+
action: 'restart-local',
|
|
607
|
+
localId: status.id,
|
|
608
|
+
port: status.port,
|
|
609
|
+
token: status.token || undefined,
|
|
610
|
+
})
|
|
611
|
+
if (!result.ok || !result.local) throw new Error(result.error || 'Failed to restart local OpenClaw.')
|
|
612
|
+
syncLocalResponse(result)
|
|
613
|
+
if (result.token) setDeployToken(result.token)
|
|
614
|
+
showMessage(`Restarted ${status.name}.`)
|
|
615
|
+
} catch (err: unknown) {
|
|
616
|
+
setError(err instanceof Error ? err.message : 'Failed to restart local OpenClaw.')
|
|
617
|
+
} finally {
|
|
618
|
+
setLoading('idle')
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const handleStopSpecificLocal = async (status: LocalDeployStatus) => {
|
|
623
|
+
setLoading('stopping-local')
|
|
624
|
+
setError('')
|
|
625
|
+
try {
|
|
626
|
+
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
627
|
+
action: 'stop-local',
|
|
628
|
+
localId: status.id,
|
|
629
|
+
})
|
|
630
|
+
if (!result.ok || !result.local) throw new Error(result.error || 'Failed to stop local OpenClaw.')
|
|
631
|
+
syncLocalResponse(result)
|
|
632
|
+
showMessage(`Stopped ${status.name}.`)
|
|
633
|
+
} catch (err: unknown) {
|
|
634
|
+
setError(err instanceof Error ? err.message : 'Failed to stop local OpenClaw.')
|
|
635
|
+
} finally {
|
|
636
|
+
setLoading('idle')
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
506
640
|
const handleGenerateBundle = async () => {
|
|
507
641
|
setLoading('generating-bundle')
|
|
508
642
|
setError('')
|
|
@@ -556,7 +690,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
556
690
|
})
|
|
557
691
|
if (!result.verify) throw new Error(result.error || 'Verification failed.')
|
|
558
692
|
const summary = result.verify.ok
|
|
559
|
-
? `Verified ${result.verify.endpoint} with ${result.verify.models.length} model${result.verify.models.length === 1 ? '' : 's'}.`
|
|
693
|
+
? (result.verify.message || `Verified ${result.verify.endpoint} with ${result.verify.models.length} model${result.verify.models.length === 1 ? '' : 's'}.`)
|
|
560
694
|
: (result.verify.error || result.verify.hint || 'Verification failed.')
|
|
561
695
|
setVerifySummary(summary)
|
|
562
696
|
await applyDeploymentPatch({
|
|
@@ -582,8 +716,12 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
582
716
|
setError('')
|
|
583
717
|
setVerifySummary('')
|
|
584
718
|
try {
|
|
719
|
+
const trimmedHost = sshHost.trim()
|
|
720
|
+
const selectedRemoteMatchesHost = !!remoteStatus?.id && !!trimmedHost && remoteStatus.target === trimmedHost
|
|
585
721
|
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
586
722
|
action: 'ssh-deploy',
|
|
723
|
+
remoteId: selectedRemoteMatchesHost ? remoteStatus?.id : undefined,
|
|
724
|
+
name: selectedRemoteMatchesHost ? remoteStatus?.name : undefined,
|
|
587
725
|
template: remoteTemplate,
|
|
588
726
|
target: remoteTarget.trim(),
|
|
589
727
|
scheme: remoteScheme,
|
|
@@ -592,7 +730,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
592
730
|
useCase,
|
|
593
731
|
exposure,
|
|
594
732
|
ssh: {
|
|
595
|
-
host:
|
|
733
|
+
host: trimmedHost,
|
|
596
734
|
user: sshUser.trim() || undefined,
|
|
597
735
|
port: sshPort,
|
|
598
736
|
keyPath: sshKeyPath.trim() || undefined,
|
|
@@ -605,19 +743,19 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
605
743
|
setBundleFile(result.bundle.files[0]?.name || '')
|
|
606
744
|
}
|
|
607
745
|
if (result.token) setDeployToken(result.token)
|
|
608
|
-
|
|
746
|
+
syncRemoteResponse(result)
|
|
609
747
|
setCommandPreview(result.commandPreview || '')
|
|
610
748
|
await applyDeploymentPatch({
|
|
611
749
|
endpoint: result.bundle?.endpoint || endpoint || undefined,
|
|
612
750
|
token: result.token || deployToken,
|
|
613
|
-
name: suggestedName || result.bundle?.title || `SSH OpenClaw ${
|
|
614
|
-
notes: `Official OpenClaw deployed over SSH to ${
|
|
751
|
+
name: suggestedName || result.bundle?.title || `SSH OpenClaw ${trimmedHost}`,
|
|
752
|
+
notes: `Official OpenClaw deployed over SSH to ${trimmedHost}.`,
|
|
615
753
|
deployment: buildRemoteDeploymentPatch({
|
|
616
754
|
method: 'ssh',
|
|
617
755
|
provider: remoteProvider,
|
|
618
756
|
lastDeployAt: Date.now(),
|
|
619
757
|
lastDeployAction: 'ssh-deploy',
|
|
620
|
-
lastDeploySummary: result.summary || `Started SSH deploy to ${
|
|
758
|
+
lastDeploySummary: result.summary || `Started SSH deploy to ${trimmedHost}.`,
|
|
621
759
|
lastDeployProcessId: result.processId || null,
|
|
622
760
|
}),
|
|
623
761
|
})
|
|
@@ -635,8 +773,13 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
635
773
|
setLoading('remote-action')
|
|
636
774
|
setError('')
|
|
637
775
|
try {
|
|
776
|
+
if (!remoteStatus?.id && !sshHost.trim()) {
|
|
777
|
+
throw new Error('Pick or configure a remote deployment first.')
|
|
778
|
+
}
|
|
638
779
|
const result = await api<DeployActionResponse>('POST', '/openclaw/deploy', {
|
|
639
780
|
action,
|
|
781
|
+
remoteId: remoteStatus?.id || undefined,
|
|
782
|
+
name: remoteStatus?.name || undefined,
|
|
640
783
|
token: action === 'remote-rotate-token' ? (deployToken.trim() || undefined) : undefined,
|
|
641
784
|
backupPath: action === 'remote-restore' ? (restoreBackupPath.trim() || undefined) : undefined,
|
|
642
785
|
ssh: {
|
|
@@ -649,7 +792,7 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
649
792
|
})
|
|
650
793
|
if (!result.ok) throw new Error(result.error || 'Remote lifecycle action failed.')
|
|
651
794
|
if (result.token) setDeployToken(result.token)
|
|
652
|
-
|
|
795
|
+
syncRemoteResponse(result)
|
|
653
796
|
setCommandPreview(result.commandPreview || '')
|
|
654
797
|
if (result.remote?.lastBackupPath) {
|
|
655
798
|
setRestoreBackupPath(result.remote.lastBackupPath)
|
|
@@ -833,6 +976,75 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
833
976
|
{localStatus.tail}
|
|
834
977
|
</pre>
|
|
835
978
|
)}
|
|
979
|
+
|
|
980
|
+
{visibleLocalStatuses.length > 0 && (
|
|
981
|
+
<div className="mt-3 space-y-2">
|
|
982
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Managed instances</div>
|
|
983
|
+
{visibleLocalStatuses.map((status) => (
|
|
984
|
+
<div key={status.id} className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-3">
|
|
985
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
986
|
+
<div>
|
|
987
|
+
<div className="text-[13px] font-600 text-text">
|
|
988
|
+
{status.name}
|
|
989
|
+
{status.isPrimary ? ' · primary' : ''}
|
|
990
|
+
</div>
|
|
991
|
+
<div className="mt-1 text-[11px] text-text-3 font-mono">
|
|
992
|
+
{status.endpoint}
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
<div className={`rounded-full px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${
|
|
996
|
+
status.running
|
|
997
|
+
? 'bg-emerald-500/10 text-emerald-300'
|
|
998
|
+
: 'bg-white/[0.05] text-text-3'
|
|
999
|
+
}`}>
|
|
1000
|
+
{status.running ? 'running' : 'idle'}
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
1004
|
+
<button
|
|
1005
|
+
type="button"
|
|
1006
|
+
onClick={() => void handleRestartSpecificLocal(status)}
|
|
1007
|
+
disabled={loading !== 'idle'}
|
|
1008
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1009
|
+
>
|
|
1010
|
+
Restart
|
|
1011
|
+
</button>
|
|
1012
|
+
<button
|
|
1013
|
+
type="button"
|
|
1014
|
+
onClick={() => void handleVerify(status.endpoint, status.token || deployToken || undefined)}
|
|
1015
|
+
disabled={loading !== 'idle'}
|
|
1016
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1017
|
+
>
|
|
1018
|
+
Verify
|
|
1019
|
+
</button>
|
|
1020
|
+
<button
|
|
1021
|
+
type="button"
|
|
1022
|
+
onClick={() => void handleStopSpecificLocal(status)}
|
|
1023
|
+
disabled={loading !== 'idle' || !status.running}
|
|
1024
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1025
|
+
>
|
|
1026
|
+
Stop
|
|
1027
|
+
</button>
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
onClick={() => onCopied(`local-endpoint-${status.id}`, status.endpoint)}
|
|
1031
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all"
|
|
1032
|
+
>
|
|
1033
|
+
{copiedKey === `local-endpoint-${status.id}` ? 'Copied endpoint' : 'Copy endpoint'}
|
|
1034
|
+
</button>
|
|
1035
|
+
<button
|
|
1036
|
+
type="button"
|
|
1037
|
+
onClick={() => onCopied(`local-token-${status.id}`, status.token || '')}
|
|
1038
|
+
disabled={!status.token}
|
|
1039
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40"
|
|
1040
|
+
>
|
|
1041
|
+
{copiedKey === `local-token-${status.id}` ? 'Copied token' : 'Copy token'}
|
|
1042
|
+
</button>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
))}
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
836
1048
|
</div>
|
|
837
1049
|
</div>
|
|
838
1050
|
)}
|
|
@@ -1093,11 +1305,47 @@ export function OpenClawDeployPanel(props: OpenClawDeployPanelProps) {
|
|
|
1093
1305
|
</div>
|
|
1094
1306
|
)}
|
|
1095
1307
|
|
|
1096
|
-
{(verifySummary || commandPreview || remoteStatus) && (
|
|
1308
|
+
{(verifySummary || commandPreview || remoteStatus || visibleRemoteStatuses.length > 0) && (
|
|
1097
1309
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-4">
|
|
1098
1310
|
{verifySummary && (
|
|
1099
1311
|
<div className="text-[12px] text-text-2 leading-relaxed">{verifySummary}</div>
|
|
1100
1312
|
)}
|
|
1313
|
+
{visibleRemoteStatuses.length > 0 && (
|
|
1314
|
+
<div className="mt-3 space-y-2">
|
|
1315
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60">Managed remote deployments</div>
|
|
1316
|
+
{visibleRemoteStatuses.map((status) => (
|
|
1317
|
+
<div key={status.id} className="rounded-[10px] border border-white/[0.05] bg-white/[0.02] px-3 py-3">
|
|
1318
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
1319
|
+
<div>
|
|
1320
|
+
<div className="text-[13px] font-600 text-text">
|
|
1321
|
+
{status.name}
|
|
1322
|
+
{status.isPrimary ? ' · primary' : ''}
|
|
1323
|
+
</div>
|
|
1324
|
+
<div className="mt-1 text-[11px] text-text-3 font-mono break-all">
|
|
1325
|
+
{status.target || 'n/a'}
|
|
1326
|
+
</div>
|
|
1327
|
+
</div>
|
|
1328
|
+
<div className="flex items-center gap-2">
|
|
1329
|
+
<div className={`rounded-full px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${
|
|
1330
|
+
status.active
|
|
1331
|
+
? 'bg-emerald-500/10 text-emerald-300'
|
|
1332
|
+
: 'bg-white/[0.05] text-text-3'
|
|
1333
|
+
}`}>
|
|
1334
|
+
{status.status}
|
|
1335
|
+
</div>
|
|
1336
|
+
<button
|
|
1337
|
+
type="button"
|
|
1338
|
+
onClick={() => handleSelectRemote(status)}
|
|
1339
|
+
className="rounded-[10px] border border-white/[0.08] bg-transparent px-3 py-1.5 text-[11px] font-700 text-text-2 cursor-pointer hover:bg-white/[0.04] transition-all"
|
|
1340
|
+
>
|
|
1341
|
+
{remoteStatus?.id === status.id ? 'Selected' : 'Select'}
|
|
1342
|
+
</button>
|
|
1343
|
+
</div>
|
|
1344
|
+
</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
))}
|
|
1347
|
+
</div>
|
|
1348
|
+
)}
|
|
1101
1349
|
{remoteStatus && (
|
|
1102
1350
|
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
|
1103
1351
|
<div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
|