@swarmclawai/swarmclaw 0.7.6 → 0.7.7

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.
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
3
+ import { useEffect, useRef, useState } from 'react'
4
4
  import { BottomSheet } from '@/components/shared/bottom-sheet'
5
5
  import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
6
6
  import { useAppStore } from '@/stores/use-app-store'
@@ -12,6 +12,7 @@ import type {
12
12
  OpenClawNode,
13
13
  OpenClawNodePairRequest,
14
14
  OpenClawPairedDevice,
15
+ GatewayProfile,
15
16
  } from '@/types'
16
17
 
17
18
  interface DiscoveryResult {
@@ -37,6 +38,17 @@ interface PairingListResult<T> {
37
38
  paired?: OpenClawPairedDevice[]
38
39
  }
39
40
 
41
+ interface GatewayImportShape {
42
+ name?: string
43
+ endpoint?: string
44
+ credentialId?: string | null
45
+ token?: string | null
46
+ notes?: string | null
47
+ tags?: string[]
48
+ isDefault?: boolean
49
+ deployment?: GatewayProfile['deployment']
50
+ }
51
+
40
52
  export function GatewaySheet() {
41
53
  const open = useAppStore((s) => s.gatewaySheetOpen)
42
54
  const setOpen = useAppStore((s) => s.setGatewaySheetOpen)
@@ -73,6 +85,8 @@ export function GatewaySheet() {
73
85
  const [invokeParamsText, setInvokeParamsText] = useState('{}')
74
86
  const [invokeResult, setInvokeResult] = useState('')
75
87
  const [invoking, setInvoking] = useState(false)
88
+ const [deployment, setDeployment] = useState<GatewayProfile['deployment'] | null>(null)
89
+ const importFileRef = useRef<HTMLInputElement>(null)
76
90
 
77
91
  useEffect(() => {
78
92
  if (!open) return
@@ -93,6 +107,7 @@ export function GatewaySheet() {
93
107
  setNotes(editing.notes || '')
94
108
  setTags((editing.tags || []).join(', '))
95
109
  setIsDefault(editing.isDefault === true)
110
+ setDeployment(editing.deployment || null)
96
111
  return
97
112
  }
98
113
  setName('')
@@ -102,6 +117,7 @@ export function GatewaySheet() {
102
117
  setNotes('')
103
118
  setTags('')
104
119
  setIsDefault(gatewayProfiles.length === 0)
120
+ setDeployment(null)
105
121
  setNodes([])
106
122
  setNodePairings([])
107
123
  setDevicePairings([])
@@ -143,10 +159,18 @@ export function GatewaySheet() {
143
159
  setNodePairings(nextNodePairings)
144
160
  setDevicePairings(nextDevicePairings)
145
161
  setPairedDevices(nextPairedDevices)
162
+ const nextStats: NonNullable<GatewayProfile['stats']> = {
163
+ nodeCount: nextNodes.length,
164
+ connectedNodeCount: nextNodes.filter((node) => node.connected).length,
165
+ pendingNodePairings: nextNodePairings.length,
166
+ pairedDeviceCount: nextPairedDevices.length,
167
+ pendingDevicePairings: nextDevicePairings.length,
168
+ }
146
169
  if (nextNodes[0]) {
147
170
  setInvokeNodeId((current) => current || nextNodes[0].nodeId)
148
171
  setInvokeCommand((current) => current || nextNodes[0].commands?.[0] || '')
149
172
  }
173
+ void api('PUT', `/gateways/${profileId}`, { stats: nextStats }).catch(() => {})
150
174
  } catch (err: unknown) {
151
175
  setNodesError(err instanceof Error ? err.message : 'Failed to load nodes for this gateway.')
152
176
  } finally {
@@ -182,6 +206,7 @@ export function GatewaySheet() {
182
206
  credentialId: nextCredentialId || null,
183
207
  notes: notes.trim() || null,
184
208
  tags: tags.split(',').map((item) => item.trim()).filter(Boolean),
209
+ deployment,
185
210
  isDefault,
186
211
  }
187
212
  if (editing) {
@@ -298,7 +323,7 @@ export function GatewaySheet() {
298
323
 
299
324
  const inputClass = 'w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow'
300
325
 
301
- const applyDeployPatch = (patch: { endpoint?: string; token?: string; name?: string; notes?: string }) => {
326
+ const applyDeployPatch = (patch: { endpoint?: string; token?: string; name?: string; notes?: string; deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null }) => {
302
327
  if (patch.endpoint) {
303
328
  setEndpoint(patch.endpoint)
304
329
  setCheckMessage('')
@@ -313,17 +338,97 @@ export function GatewaySheet() {
313
338
  if (patch.notes && !notes.trim()) {
314
339
  setNotes(patch.notes)
315
340
  }
341
+ if (patch.deployment) {
342
+ setDeployment((current) => ({
343
+ ...(current || {}),
344
+ ...(patch.deployment as GatewayProfile['deployment']),
345
+ }))
346
+ }
347
+ }
348
+
349
+ const handleExportGateway = () => {
350
+ const payload: GatewayImportShape = {
351
+ name: name.trim() || 'OpenClaw Gateway',
352
+ endpoint: endpoint.trim() || 'http://localhost:18789',
353
+ credentialId: credentialId || null,
354
+ token: tokenDraft.trim() || null,
355
+ notes: notes.trim() || null,
356
+ tags: tags.split(',').map((item) => item.trim()).filter(Boolean),
357
+ isDefault,
358
+ deployment,
359
+ }
360
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
361
+ const url = URL.createObjectURL(blob)
362
+ const link = document.createElement('a')
363
+ link.href = url
364
+ link.download = `${(payload.name || 'openclaw-gateway').replace(/[^a-zA-Z0-9_-]/g, '_')}.gateway.json`
365
+ link.click()
366
+ URL.revokeObjectURL(url)
367
+ toast.success('Gateway config exported')
368
+ }
369
+
370
+ const handleImportGateway = (event: React.ChangeEvent<HTMLInputElement>) => {
371
+ const file = event.target.files?.[0]
372
+ if (!file) return
373
+ const reader = new FileReader()
374
+ reader.onload = (loadEvent) => {
375
+ try {
376
+ const parsed = JSON.parse(String(loadEvent.target?.result || '{}')) as GatewayImportShape
377
+ setName(typeof parsed.name === 'string' ? parsed.name : '')
378
+ setEndpoint(typeof parsed.endpoint === 'string' ? parsed.endpoint : 'http://localhost:18789')
379
+ setCredentialId(typeof parsed.credentialId === 'string' ? parsed.credentialId : null)
380
+ setTokenDraft(typeof parsed.token === 'string' ? parsed.token : '')
381
+ setNotes(typeof parsed.notes === 'string' ? parsed.notes : '')
382
+ setTags(Array.isArray(parsed.tags) ? parsed.tags.join(', ') : '')
383
+ setIsDefault(parsed.isDefault === true)
384
+ setDeployment(parsed.deployment || null)
385
+ setCheckMessage('')
386
+ toast.success('Gateway config imported into this form')
387
+ } catch {
388
+ toast.error('Invalid gateway JSON')
389
+ } finally {
390
+ event.target.value = ''
391
+ }
392
+ }
393
+ reader.readAsText(file)
316
394
  }
317
395
 
318
396
  return (
319
397
  <BottomSheet open={open} onClose={onClose} wide>
398
+ <input
399
+ ref={importFileRef}
400
+ type="file"
401
+ accept="application/json,.json"
402
+ onChange={handleImportGateway}
403
+ className="hidden"
404
+ />
320
405
  <div className="mb-10">
321
- <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
322
- {editing ? 'Edit Gateway' : 'New Gateway'}
323
- </h2>
324
- <p className="text-[14px] text-text-3">
325
- First-class OpenClaw gateway profiles for local or remote control planes.
326
- </p>
406
+ <div className="flex flex-wrap items-start justify-between gap-3">
407
+ <div>
408
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
409
+ {editing ? 'Edit Gateway' : 'New Gateway'}
410
+ </h2>
411
+ <p className="text-[14px] text-text-3">
412
+ First-class OpenClaw gateway profiles for local or remote control planes.
413
+ </p>
414
+ </div>
415
+ <div className="flex flex-wrap gap-2">
416
+ <button
417
+ type="button"
418
+ onClick={() => importFileRef.current?.click()}
419
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 hover:bg-white/[0.04] transition-all cursor-pointer"
420
+ >
421
+ Import JSON
422
+ </button>
423
+ <button
424
+ type="button"
425
+ onClick={handleExportGateway}
426
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 hover:bg-white/[0.04] transition-all cursor-pointer"
427
+ >
428
+ Export JSON
429
+ </button>
430
+ </div>
431
+ </div>
327
432
  </div>
328
433
 
329
434
  <div className="mb-6">
@@ -350,6 +455,7 @@ export function GatewaySheet() {
350
455
  <OpenClawDeployPanel
351
456
  endpoint={endpoint}
352
457
  token={tokenDraft}
458
+ deployment={deployment}
353
459
  suggestedName={name || null}
354
460
  title="Deploy OpenClaw From SwarmClaw"
355
461
  description="Use official OpenClaw sources only. Start it on this host, or generate a pre-configured remote bundle for VPS and hosted deployments."
@@ -357,6 +463,32 @@ export function GatewaySheet() {
357
463
  />
358
464
  </div>
359
465
 
466
+ {deployment && (
467
+ <div className="mb-6 rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4">
468
+ <div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/70 mb-2">Deploy metadata</div>
469
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-[12px] text-text-3/75">
470
+ <div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
471
+ <div className="uppercase tracking-[0.08em] text-text-3/55">Method</div>
472
+ <div className="mt-1 text-text-2">{deployment.method || 'manual'}</div>
473
+ </div>
474
+ <div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
475
+ <div className="uppercase tracking-[0.08em] text-text-3/55">Use case</div>
476
+ <div className="mt-1 text-text-2">{deployment.useCase || 'general'}</div>
477
+ </div>
478
+ <div className="rounded-[12px] border border-white/[0.06] bg-surface px-3 py-3">
479
+ <div className="uppercase tracking-[0.08em] text-text-3/55">Exposure</div>
480
+ <div className="mt-1 text-text-2">{deployment.exposure || 'manual'}</div>
481
+ </div>
482
+ </div>
483
+ {deployment.lastDeploySummary && (
484
+ <p className="mt-3 text-[12px] text-text-3 leading-relaxed">{deployment.lastDeploySummary}</p>
485
+ )}
486
+ {deployment.lastVerifiedMessage && (
487
+ <p className="mt-2 text-[12px] text-text-3 leading-relaxed">{deployment.lastVerifiedMessage}</p>
488
+ )}
489
+ </div>
490
+ )}
491
+
360
492
  {discoveries.length > 0 && (
361
493
  <div className="mb-6">
362
494
  <div className="text-[12px] text-text-3/70 mb-2">Detected healthy gateways</div>