@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -54,6 +54,9 @@ interface ConfiguredProvider {
|
|
|
54
54
|
endpoint: string | null
|
|
55
55
|
defaultModel: string
|
|
56
56
|
gatewayProfileId: string | null
|
|
57
|
+
notes?: string | null
|
|
58
|
+
tags?: string[]
|
|
59
|
+
deployment?: GatewayProfile['deployment'] | null
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
interface StarterDraftAgent {
|
|
@@ -88,6 +91,20 @@ const CONNECTOR_ICONS = [
|
|
|
88
91
|
{ name: 'Telegram', icon: 'T' },
|
|
89
92
|
{ name: 'WhatsApp', icon: 'W' },
|
|
90
93
|
]
|
|
94
|
+
const OPENCLAW_USE_CASE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['useCase']>, string> = {
|
|
95
|
+
'local-dev': 'Local Dev',
|
|
96
|
+
'single-vps': 'Single VPS',
|
|
97
|
+
'private-tailnet': 'Private Tailnet',
|
|
98
|
+
'browser-heavy': 'Browser Heavy',
|
|
99
|
+
'team-control': 'Team Control',
|
|
100
|
+
}
|
|
101
|
+
const OPENCLAW_EXPOSURE_LABELS: Record<NonNullable<NonNullable<GatewayProfile['deployment']>['exposure']>, string> = {
|
|
102
|
+
'private-lan': 'Private LAN',
|
|
103
|
+
tailscale: 'Tailscale',
|
|
104
|
+
caddy: 'Caddy',
|
|
105
|
+
nginx: 'Nginx',
|
|
106
|
+
'ssh-tunnel': 'SSH Tunnel',
|
|
107
|
+
}
|
|
91
108
|
|
|
92
109
|
function stepIndex(step: SetupStep): number {
|
|
93
110
|
if (step === 'connect') return STEP_ORDER.indexOf('providers')
|
|
@@ -224,6 +241,12 @@ function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[
|
|
|
224
241
|
{cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
|
|
225
242
|
? `· ${formatEndpointHost(cp.endpoint)}`
|
|
226
243
|
: ''}
|
|
244
|
+
{cp.provider === 'openclaw' && cp.deployment?.useCase
|
|
245
|
+
? ` · ${OPENCLAW_USE_CASE_LABELS[cp.deployment.useCase]}`
|
|
246
|
+
: ''}
|
|
247
|
+
{cp.provider === 'openclaw' && cp.deployment?.exposure
|
|
248
|
+
? ` · ${OPENCLAW_EXPOSURE_LABELS[cp.deployment.exposure]}`
|
|
249
|
+
: ''}
|
|
227
250
|
{cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
|
|
228
251
|
</span>
|
|
229
252
|
</span>
|
|
@@ -332,6 +355,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
332
355
|
const [endpoint, setEndpoint] = useState('')
|
|
333
356
|
const [apiKey, setApiKey] = useState('')
|
|
334
357
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
358
|
+
const [providerNotes, setProviderNotes] = useState('')
|
|
359
|
+
const [providerTags, setProviderTags] = useState<string[]>([])
|
|
360
|
+
const [providerDeployment, setProviderDeployment] = useState<GatewayProfile['deployment'] | null>(null)
|
|
335
361
|
const [checkState, setCheckState] = useState<CheckState>('idle')
|
|
336
362
|
const [checkMessage, setCheckMessage] = useState('')
|
|
337
363
|
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
@@ -382,6 +408,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
382
408
|
setEndpoint('')
|
|
383
409
|
setApiKey('')
|
|
384
410
|
setCredentialId(null)
|
|
411
|
+
setProviderNotes('')
|
|
412
|
+
setProviderTags([])
|
|
413
|
+
setProviderDeployment(null)
|
|
385
414
|
setCheckState('idle')
|
|
386
415
|
setCheckMessage('')
|
|
387
416
|
setCheckErrorCode(null)
|
|
@@ -432,6 +461,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
432
461
|
setEndpoint(meta?.defaultEndpoint || '')
|
|
433
462
|
setApiKey('')
|
|
434
463
|
setCredentialId(null)
|
|
464
|
+
setProviderNotes('')
|
|
465
|
+
setProviderTags([])
|
|
466
|
+
setProviderDeployment(null)
|
|
435
467
|
setCheckState('idle')
|
|
436
468
|
setCheckMessage('')
|
|
437
469
|
setCheckErrorCode(null)
|
|
@@ -445,6 +477,8 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
445
477
|
endpoint?: string
|
|
446
478
|
token?: string
|
|
447
479
|
name?: string
|
|
480
|
+
notes?: string
|
|
481
|
+
deployment?: GatewayProfile['deployment'] | Record<string, unknown> | null
|
|
448
482
|
}) => {
|
|
449
483
|
if (patch.endpoint) {
|
|
450
484
|
setEndpoint(patch.endpoint)
|
|
@@ -456,6 +490,22 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
456
490
|
if (patch.name && (!providerLabel.trim() || providerLabel.trim() === (selectedProvider?.name || ''))) {
|
|
457
491
|
setProviderLabel(patch.name)
|
|
458
492
|
}
|
|
493
|
+
if (patch.notes) {
|
|
494
|
+
setProviderNotes(patch.notes)
|
|
495
|
+
}
|
|
496
|
+
if (patch.deployment) {
|
|
497
|
+
const nextDeployment = patch.deployment as GatewayProfile['deployment']
|
|
498
|
+
setProviderDeployment((current) => ({
|
|
499
|
+
...(current || {}),
|
|
500
|
+
...(nextDeployment || {}),
|
|
501
|
+
}))
|
|
502
|
+
setProviderTags((current) => Array.from(new Set([
|
|
503
|
+
...current,
|
|
504
|
+
'onboarding',
|
|
505
|
+
...(nextDeployment?.useCase ? [nextDeployment.useCase] : []),
|
|
506
|
+
...(nextDeployment?.exposure ? [nextDeployment.exposure] : []),
|
|
507
|
+
])))
|
|
508
|
+
}
|
|
459
509
|
setCheckState('idle')
|
|
460
510
|
setCheckMessage('')
|
|
461
511
|
setCheckErrorCode(null)
|
|
@@ -552,6 +602,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
552
602
|
endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
|
|
553
603
|
defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
|
|
554
604
|
gatewayProfileId: null,
|
|
605
|
+
notes: providerNotes.trim() || null,
|
|
606
|
+
tags: providerTags,
|
|
607
|
+
deployment: providerDeployment,
|
|
555
608
|
}
|
|
556
609
|
|
|
557
610
|
const nextConfigured = [...configuredProviders, configuredProvider]
|
|
@@ -645,8 +698,13 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
645
698
|
name: configuredProvider.name,
|
|
646
699
|
endpoint: normalizedEndpoint,
|
|
647
700
|
credentialId: configuredProvider.credentialId || null,
|
|
648
|
-
tags: [
|
|
649
|
-
|
|
701
|
+
tags: Array.from(new Set([
|
|
702
|
+
'onboarding',
|
|
703
|
+
...(configuredProvider.tags || []),
|
|
704
|
+
])),
|
|
705
|
+
notes: configuredProvider.notes || `Created during setup for ${configuredProvider.name}.`,
|
|
706
|
+
deployment: configuredProvider.deployment || null,
|
|
707
|
+
status: configuredProvider.deployment?.lastVerifiedOk ? 'healthy' : 'pending',
|
|
650
708
|
isDefault: shouldCreateDefault,
|
|
651
709
|
})
|
|
652
710
|
gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
|
|
@@ -1084,6 +1142,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1084
1142
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1085
1143
|
If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
|
|
1086
1144
|
</p>
|
|
1145
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1146
|
+
Safer remote defaults: use <code className="text-text-2">private-tailnet</code> with <code className="text-text-2">tailscale</code> or <code className="text-text-2">ssh-tunnel</code> unless you intentionally want public HTTPS ingress.
|
|
1147
|
+
</p>
|
|
1087
1148
|
</div>
|
|
1088
1149
|
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1089
1150
|
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Safe defaults</div>
|
|
@@ -1093,6 +1154,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1093
1154
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1094
1155
|
Local quickstart uses the bundled official OpenClaw CLI. Remote quickstart uses the official OpenClaw Docker image or the official repo for managed hosts.
|
|
1095
1156
|
</p>
|
|
1157
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1158
|
+
Choose <code className="text-text-2">local-dev</code> for one-machine setup, <code className="text-text-2">single-vps</code> for most hosted installs, or <code className="text-text-2">private-tailnet</code> when the gateway should stay private.
|
|
1159
|
+
</p>
|
|
1096
1160
|
</div>
|
|
1097
1161
|
</div>
|
|
1098
1162
|
|
|
@@ -1104,6 +1168,9 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1104
1168
|
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1105
1169
|
Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
|
|
1106
1170
|
</p>
|
|
1171
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1172
|
+
Use <code className="text-text-2">caddy</code> or <code className="text-text-2">nginx</code> only when you intentionally want HTTPS/public ingress managed on the gateway side.
|
|
1173
|
+
</p>
|
|
1107
1174
|
</div>
|
|
1108
1175
|
</div>
|
|
1109
1176
|
)}
|
|
@@ -1314,6 +1381,12 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
1314
1381
|
{configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
|
|
1315
1382
|
? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
|
|
1316
1383
|
: ''}
|
|
1384
|
+
{configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.useCase
|
|
1385
|
+
? ` · ${OPENCLAW_USE_CASE_LABELS[configuredProvider.deployment.useCase]}`
|
|
1386
|
+
: ''}
|
|
1387
|
+
{configuredProvider.provider === 'openclaw' && configuredProvider.deployment?.exposure
|
|
1388
|
+
? ` · ${OPENCLAW_EXPOSURE_LABELS[configuredProvider.deployment.exposure]}`
|
|
1389
|
+
: ''}
|
|
1317
1390
|
{configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
|
|
1318
1391
|
</option>
|
|
1319
1392
|
))}
|
|
@@ -136,10 +136,12 @@ export function ChatArea() {
|
|
|
136
136
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
137
137
|
setMessagesLoading(false)
|
|
138
138
|
})
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
|
|
140
|
+
const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
|
|
141
|
+
if (sessionAtLoad?.active) {
|
|
141
142
|
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
|
|
142
143
|
}
|
|
144
|
+
|
|
143
145
|
// Refresh active state from server so returning to a session restores typing indicator.
|
|
144
146
|
loadSessions().then(() => {
|
|
145
147
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
@@ -148,6 +150,7 @@ export function ChatArea() {
|
|
|
148
150
|
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
|
|
149
151
|
}
|
|
150
152
|
}).catch((err) => console.error('Failed to refresh messages:', err))
|
|
153
|
+
|
|
151
154
|
devServer(requestedSessionId, 'status').then((r) => {
|
|
152
155
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
153
156
|
setDevServer(r.running ? r : null)
|
|
@@ -155,23 +158,31 @@ export function ChatArea() {
|
|
|
155
158
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
156
159
|
setDevServer(null)
|
|
157
160
|
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} else {
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
cancelled = true
|
|
164
|
+
}
|
|
165
|
+
}, [loadSessions, sessionId, setDevServer, setMessages])
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!sessionId) return
|
|
169
|
+
let cancelled = false
|
|
170
|
+
if (!sessionHasBrowserPlugin) {
|
|
169
171
|
setBrowserActive(false)
|
|
172
|
+
return
|
|
170
173
|
}
|
|
174
|
+
checkBrowser(sessionId).then((r) => {
|
|
175
|
+
if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
|
|
176
|
+
setBrowserActive(r.active)
|
|
177
|
+
}).catch((err) => {
|
|
178
|
+
if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
|
|
179
|
+
console.error('Browser check failed:', err)
|
|
180
|
+
setBrowserActive(false)
|
|
181
|
+
})
|
|
171
182
|
return () => {
|
|
172
183
|
cancelled = true
|
|
173
184
|
}
|
|
174
|
-
}, [
|
|
185
|
+
}, [sessionHasBrowserPlugin, sessionId])
|
|
175
186
|
|
|
176
187
|
// Auto-poll messages for sessions that are actively running on the server
|
|
177
188
|
const isServerActive = session?.active === true
|
|
@@ -216,10 +227,16 @@ export function ChatArea() {
|
|
|
216
227
|
shouldPollMessages ? 2000 : undefined,
|
|
217
228
|
)
|
|
218
229
|
|
|
219
|
-
//
|
|
230
|
+
// Keep the local typing indicator aligned with the server's active state
|
|
220
231
|
useEffect(() => {
|
|
221
232
|
if (!sessionId) return
|
|
222
233
|
const state = useChatStore.getState()
|
|
234
|
+
if (isServerActive) {
|
|
235
|
+
if (!state.streaming && !state.streamText) {
|
|
236
|
+
useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamText: '' })
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
223
240
|
if (
|
|
224
241
|
!isServerActive
|
|
225
242
|
&& state.streaming
|
|
@@ -230,7 +247,7 @@ export function ChatArea() {
|
|
|
230
247
|
fetchMessages(sessionId).then(setMessages).catch(() => {})
|
|
231
248
|
useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '' })
|
|
232
249
|
}
|
|
233
|
-
}, [isServerActive, sessionId])
|
|
250
|
+
}, [isServerActive, sessionId, setMessages])
|
|
234
251
|
|
|
235
252
|
// Poll browser status while session has browser tools
|
|
236
253
|
const hasBrowserTool = session?.plugins?.includes('browser')
|
|
@@ -255,7 +272,7 @@ export function ChatArea() {
|
|
|
255
272
|
if (!sessionId) return
|
|
256
273
|
await devServer(sessionId, 'stop')
|
|
257
274
|
setDevServer(null)
|
|
258
|
-
}, [sessionId])
|
|
275
|
+
}, [sessionId, setDevServer])
|
|
259
276
|
|
|
260
277
|
const handleClear = useCallback(async () => {
|
|
261
278
|
setConfirmClear(false)
|
|
@@ -263,7 +280,7 @@ export function ChatArea() {
|
|
|
263
280
|
await clearMessages(sessionId)
|
|
264
281
|
setMessages([])
|
|
265
282
|
loadSessions()
|
|
266
|
-
}, [sessionId])
|
|
283
|
+
}, [loadSessions, sessionId, setMessages])
|
|
267
284
|
|
|
268
285
|
const handleDelete = useCallback(async () => {
|
|
269
286
|
setConfirmDelete(false)
|
|
@@ -271,7 +288,7 @@ export function ChatArea() {
|
|
|
271
288
|
await deleteChat(sessionId)
|
|
272
289
|
removeSessionFromStore(sessionId)
|
|
273
290
|
setCurrentSession(null)
|
|
274
|
-
}, [sessionId])
|
|
291
|
+
}, [removeSessionFromStore, sessionId, setCurrentSession])
|
|
275
292
|
|
|
276
293
|
const handlePrompt = useCallback((text: string) => {
|
|
277
294
|
sendMessage(text)
|
|
@@ -280,12 +280,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
280
280
|
const fromDelegateOpenCode = session.delegateResumeIds?.opencode
|
|
281
281
|
? { label: 'OpenCode', id: session.delegateResumeIds.opencode, command: `opencode run \"<task>\" --session ${session.delegateResumeIds.opencode}` }
|
|
282
282
|
: null
|
|
283
|
+
const fromDelegateGemini = session.delegateResumeIds?.gemini
|
|
284
|
+
? { label: 'Gemini', id: session.delegateResumeIds.gemini, command: `gemini --resume ${session.delegateResumeIds.gemini} --prompt \"<task>\"` }
|
|
285
|
+
: null
|
|
283
286
|
return fromSessionClaude
|
|
284
287
|
|| fromSessionCodex
|
|
285
288
|
|| fromSessionOpenCode
|
|
286
289
|
|| fromDelegateClaude
|
|
287
290
|
|| fromDelegateCodex
|
|
288
291
|
|| fromDelegateOpenCode
|
|
292
|
+
|| fromDelegateGemini
|
|
289
293
|
|| null
|
|
290
294
|
}, [session.claudeSessionId, session.codexThreadId, session.opencodeSessionId, session.delegateResumeIds])
|
|
291
295
|
|
|
@@ -23,5 +23,18 @@ describe('parseTaskCompletion', () => {
|
|
|
23
23
|
assert.equal(parsed?.reportPath, 'data/task-reports/abc12345.md')
|
|
24
24
|
assert.equal(parsed?.workingDir, '/tmp/work')
|
|
25
25
|
})
|
|
26
|
-
})
|
|
27
26
|
|
|
27
|
+
it('captures Gemini resume lines from task completion payloads', () => {
|
|
28
|
+
const text = [
|
|
29
|
+
'Task completed: **[Ship follow-up](#task:task-gemini)**',
|
|
30
|
+
'',
|
|
31
|
+
'Gemini session: `gemini-session-7`',
|
|
32
|
+
'',
|
|
33
|
+
'All done.',
|
|
34
|
+
].join('\n')
|
|
35
|
+
const parsed = parseTaskCompletion(text)
|
|
36
|
+
|
|
37
|
+
assert.ok(parsed)
|
|
38
|
+
assert.equal(parsed?.resumeInfo, 'Gemini session: `gemini-session-7`')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -169,7 +169,7 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
|
|
|
169
169
|
}
|
|
170
170
|
} else if (section.startsWith('Task report: ')) {
|
|
171
171
|
reportPath = section.replace('Task report: ', '').replace(/^`|`$/g, '')
|
|
172
|
-
} else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
|
|
172
|
+
} else if (/^(Claude session|Codex thread|OpenCode session|Gemini session|CLI session):/.test(section)) {
|
|
173
173
|
resumeInfo = section
|
|
174
174
|
} else if (section.trim()) {
|
|
175
175
|
resultParts.push(section)
|
|
@@ -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
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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>
|