@swarmclawai/swarmclaw 0.7.3 → 0.7.4
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +3 -1
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +10 -4
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createMemory } from '@/lib/memory'
|
|
6
|
+
import { getMemoryTierForCategory } from '@/lib/memory-presentation'
|
|
6
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
8
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
@@ -24,6 +25,7 @@ export function MemorySheet() {
|
|
|
24
25
|
const [title, setTitle] = useState('')
|
|
25
26
|
const [content, setContent] = useState('')
|
|
26
27
|
const [category, setCategory] = useState('note')
|
|
28
|
+
const [tier, setTier] = useState<'working' | 'durable' | 'archive'>(getMemoryTierForCategory('note'))
|
|
27
29
|
const [agentId, setAgentId] = useState<string | null>(defaultAgentId)
|
|
28
30
|
const [sharedWith, setSharedWith] = useState<string[]>([])
|
|
29
31
|
const [saving, setSaving] = useState(false)
|
|
@@ -36,6 +38,7 @@ export function MemorySheet() {
|
|
|
36
38
|
setTitle('')
|
|
37
39
|
setContent('')
|
|
38
40
|
setCategory('note')
|
|
41
|
+
setTier(getMemoryTierForCategory('note'))
|
|
39
42
|
setSaving(false)
|
|
40
43
|
} else if (!open && prevOpen) {
|
|
41
44
|
setPrevOpen(false)
|
|
@@ -56,6 +59,11 @@ export function MemorySheet() {
|
|
|
56
59
|
agentId,
|
|
57
60
|
sessionId: null,
|
|
58
61
|
sharedWith: sharedWith.length ? sharedWith : undefined,
|
|
62
|
+
metadata: {
|
|
63
|
+
tier,
|
|
64
|
+
scope: agentId ? 'agent' : 'global',
|
|
65
|
+
visibility: agentId ? (sharedWith.length ? 'shared' : 'private') : 'global',
|
|
66
|
+
},
|
|
59
67
|
})
|
|
60
68
|
triggerRefresh()
|
|
61
69
|
onClose()
|
|
@@ -77,7 +85,7 @@ export function MemorySheet() {
|
|
|
77
85
|
|
|
78
86
|
{/* Agent selector */}
|
|
79
87
|
<div className="mb-6">
|
|
80
|
-
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
88
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Visibility</label>
|
|
81
89
|
<div className="flex gap-2 flex-wrap">
|
|
82
90
|
<button
|
|
83
91
|
onClick={() => setAgentId(null)}
|
|
@@ -111,12 +119,12 @@ export function MemorySheet() {
|
|
|
111
119
|
</div>
|
|
112
120
|
{selectedAgent && (
|
|
113
121
|
<p className="text-[11px] text-text-3/50 mt-2">
|
|
114
|
-
|
|
122
|
+
Owned by <span className="text-text-2">{selectedAgent.name}</span>. Add collaborators below if other agents should be able to recall it too.
|
|
115
123
|
</p>
|
|
116
124
|
)}
|
|
117
125
|
{!agentId && (
|
|
118
126
|
<p className="text-[11px] text-text-3/50 mt-2">
|
|
119
|
-
Global memories are accessible to
|
|
127
|
+
Global memories are accessible to every agent in the workspace.
|
|
120
128
|
</p>
|
|
121
129
|
)}
|
|
122
130
|
</div>
|
|
@@ -167,7 +175,10 @@ export function MemorySheet() {
|
|
|
167
175
|
{CATEGORIES.map((c) => (
|
|
168
176
|
<button
|
|
169
177
|
key={c}
|
|
170
|
-
onClick={() =>
|
|
178
|
+
onClick={() => {
|
|
179
|
+
setCategory(c)
|
|
180
|
+
setTier(getMemoryTierForCategory(c))
|
|
181
|
+
}}
|
|
171
182
|
className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 capitalize cursor-pointer transition-all border-none
|
|
172
183
|
${category === c
|
|
173
184
|
? 'bg-accent-soft text-accent-bright'
|
|
@@ -180,6 +191,23 @@ export function MemorySheet() {
|
|
|
180
191
|
</div>
|
|
181
192
|
</div>
|
|
182
193
|
|
|
194
|
+
<div className="mb-6">
|
|
195
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Tier</label>
|
|
196
|
+
<select
|
|
197
|
+
value={tier}
|
|
198
|
+
onChange={(e) => setTier(e.target.value as typeof tier)}
|
|
199
|
+
className={inputClass}
|
|
200
|
+
style={{ fontFamily: 'inherit' }}
|
|
201
|
+
>
|
|
202
|
+
<option value="working">Working: short-horizon, active context</option>
|
|
203
|
+
<option value="durable">Durable: keep this around as reusable knowledge</option>
|
|
204
|
+
<option value="archive">Archive: preserve, but keep less salient</option>
|
|
205
|
+
</select>
|
|
206
|
+
<p className="text-[11px] text-text-3/50 mt-2">
|
|
207
|
+
Tier controls how aggressively this memory should stay in active recall.
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
183
211
|
{/* Content */}
|
|
184
212
|
<div className="mb-8">
|
|
185
213
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Content</label>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from 'react'
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
6
|
import { updateAgent } from '@/lib/agents'
|
|
@@ -102,7 +102,12 @@ export function ProjectDetail() {
|
|
|
102
102
|
const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
103
103
|
|
|
104
104
|
const [assignPickerOpen, setAssignPickerOpen] = useState(false)
|
|
105
|
-
const now = Date.now()
|
|
105
|
+
const [now, setNow] = useState(() => Date.now())
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
109
|
+
return () => window.clearInterval(intervalId)
|
|
110
|
+
}, [])
|
|
106
111
|
|
|
107
112
|
const project = activeProjectFilter ? projects[activeProjectFilter] : null
|
|
108
113
|
|
|
@@ -10,19 +10,27 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
10
10
|
const providerConfigs = useAppStore((s) => s.providerConfigs)
|
|
11
11
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
12
12
|
const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
|
|
13
|
+
const gatewayProfiles = useAppStore((s) => s.gatewayProfiles)
|
|
14
|
+
const loadGatewayProfiles = useAppStore((s) => s.loadGatewayProfiles)
|
|
15
|
+
const externalAgents = useAppStore((s) => s.externalAgents)
|
|
16
|
+
const loadExternalAgents = useAppStore((s) => s.loadExternalAgents)
|
|
13
17
|
const credentials = useAppStore((s) => s.credentials)
|
|
14
18
|
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
15
19
|
const setProviderSheetOpen = useAppStore((s) => s.setProviderSheetOpen)
|
|
16
20
|
const setEditingProviderId = useAppStore((s) => s.setEditingProviderId)
|
|
21
|
+
const setGatewaySheetOpen = useAppStore((s) => s.setGatewaySheetOpen)
|
|
22
|
+
const setEditingGatewayId = useAppStore((s) => s.setEditingGatewayId)
|
|
17
23
|
const [loaded, setLoaded] = useState(false)
|
|
18
24
|
|
|
19
25
|
const refresh = useCallback(async () => {
|
|
20
|
-
await Promise.all([loadProviders(), loadProviderConfigs(), loadCredentials()])
|
|
26
|
+
await Promise.all([loadProviders(), loadProviderConfigs(), loadGatewayProfiles(), loadExternalAgents(), loadCredentials()])
|
|
21
27
|
setLoaded(true)
|
|
22
|
-
}, [loadProviders, loadProviderConfigs, loadCredentials])
|
|
28
|
+
}, [loadProviders, loadProviderConfigs, loadGatewayProfiles, loadExternalAgents, loadCredentials])
|
|
23
29
|
|
|
24
30
|
useEffect(() => { void refresh() }, [refresh])
|
|
25
31
|
useWs('providers', loadProviders, 20_000)
|
|
32
|
+
useWs('gateways', loadGatewayProfiles, 20_000)
|
|
33
|
+
useWs('external_agents', loadExternalAgents, 20_000)
|
|
26
34
|
|
|
27
35
|
const handleEdit = (id: string) => {
|
|
28
36
|
setEditingProviderId(id)
|
|
@@ -41,6 +49,23 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
41
49
|
await loadProviderConfigs()
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
const handleEditGateway = (id: string | null) => {
|
|
53
|
+
setEditingGatewayId(id)
|
|
54
|
+
setGatewaySheetOpen(true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleDeleteGateway = async (e: React.MouseEvent, id: string) => {
|
|
58
|
+
e.stopPropagation()
|
|
59
|
+
await api('DELETE', `/gateways/${id}`)
|
|
60
|
+
await loadGatewayProfiles()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleHealthCheckGateway = async (e: React.MouseEvent, id: string) => {
|
|
64
|
+
e.stopPropagation()
|
|
65
|
+
await api('GET', `/gateways/${id}/health`)
|
|
66
|
+
await loadGatewayProfiles()
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
// Merge built-in providers with custom configs
|
|
45
70
|
const builtinItems = providers.map((p) => ({
|
|
46
71
|
id: p.id,
|
|
@@ -74,6 +99,18 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
74
99
|
|
|
75
100
|
return (
|
|
76
101
|
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
102
|
+
<div className="mb-4 flex items-center justify-between">
|
|
103
|
+
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Model Providers</div>
|
|
104
|
+
{!inSidebar && (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => handleEditGateway(null)}
|
|
108
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-700 text-text-2 hover:bg-white/[0.04] transition-all cursor-pointer"
|
|
109
|
+
>
|
|
110
|
+
+ Gateway
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
77
114
|
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
78
115
|
{allItems.map((item, idx) => (
|
|
79
116
|
<div
|
|
@@ -139,6 +176,125 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
139
176
|
</div>
|
|
140
177
|
))}
|
|
141
178
|
</div>
|
|
179
|
+
|
|
180
|
+
<div className="mt-8 mb-4 flex items-center justify-between">
|
|
181
|
+
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">OpenClaw Gateways</div>
|
|
182
|
+
{!inSidebar && (
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => handleEditGateway(null)}
|
|
186
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-700 text-text-2 hover:bg-white/[0.04] transition-all cursor-pointer"
|
|
187
|
+
>
|
|
188
|
+
+ New Gateway
|
|
189
|
+
</button>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
193
|
+
{gatewayProfiles.map((gateway, idx) => (
|
|
194
|
+
<div
|
|
195
|
+
key={gateway.id}
|
|
196
|
+
role="button"
|
|
197
|
+
tabIndex={0}
|
|
198
|
+
onClick={() => handleEditGateway(gateway.id)}
|
|
199
|
+
onKeyDown={(e) => {
|
|
200
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
handleEditGateway(gateway.id)
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
className="w-full text-left p-4 rounded-[14px] border transition-all duration-200
|
|
206
|
+
cursor-pointer hover:bg-white/[0.02] bg-surface border-white/[0.06] hover:border-white/[0.12] hover:scale-[1.01]"
|
|
207
|
+
style={{
|
|
208
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
209
|
+
animationDelay: `${(allItems.length + idx) * 0.04}s`,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div className="flex items-center justify-between mb-2">
|
|
213
|
+
<div className="min-w-0">
|
|
214
|
+
<div className="font-display text-[14px] font-600 text-text truncate">{gateway.name}</div>
|
|
215
|
+
<div className="text-[11px] text-text-3/60 font-mono truncate">{gateway.endpoint}</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
218
|
+
{gateway.isDefault && (
|
|
219
|
+
<span className="text-[10px] font-700 px-2 py-0.5 rounded-[5px] bg-accent-bright/10 text-accent-bright uppercase tracking-wider">Default</span>
|
|
220
|
+
)}
|
|
221
|
+
<span className={`w-2 h-2 rounded-full ${
|
|
222
|
+
gateway.status === 'healthy'
|
|
223
|
+
? 'bg-emerald-400'
|
|
224
|
+
: gateway.status === 'degraded'
|
|
225
|
+
? 'bg-amber-400'
|
|
226
|
+
: gateway.status === 'offline'
|
|
227
|
+
? 'bg-red-400'
|
|
228
|
+
: 'bg-white/10'
|
|
229
|
+
}`} />
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="text-[12px] text-text-3/70">
|
|
233
|
+
{gateway.tags?.length ? gateway.tags.join(', ') : (gateway.notes || 'Dedicated OpenClaw control plane')}
|
|
234
|
+
</div>
|
|
235
|
+
{!inSidebar && (
|
|
236
|
+
<div className="mt-3 flex items-center gap-2">
|
|
237
|
+
<button onClick={(e) => void handleHealthCheckGateway(e, gateway.id)} className="px-2.5 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-700 text-text-2 hover:bg-white/[0.04] cursor-pointer transition-all">
|
|
238
|
+
Health
|
|
239
|
+
</button>
|
|
240
|
+
<button onClick={(e) => handleDeleteGateway(e, gateway.id)} className="px-2.5 py-1.5 rounded-[8px] border border-red-400/20 bg-red-400/[0.06] text-[11px] font-700 text-red-300 hover:bg-red-400/[0.1] cursor-pointer transition-all">
|
|
241
|
+
Delete
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
))}
|
|
247
|
+
{gatewayProfiles.length === 0 && (
|
|
248
|
+
<div className="p-4 rounded-[14px] border border-dashed border-white/[0.08] text-[13px] text-text-3/70">
|
|
249
|
+
No gateway profiles yet. Add one to route OpenClaw agents by named control plane instead of a singleton default.
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{!inSidebar && (
|
|
255
|
+
<>
|
|
256
|
+
<div className="mt-8 mb-4 flex items-center justify-between">
|
|
257
|
+
<div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">External Agent Runtimes</div>
|
|
258
|
+
<div className="text-[11px] text-text-3/60">Direct registration + heartbeat</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="mb-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-4 py-3 text-[12px] text-text-3/70">
|
|
261
|
+
External workers can register themselves at <code className="text-text-2">/api/external-agents/register</code> and then send heartbeats to
|
|
262
|
+
{' '}
|
|
263
|
+
<code className="text-text-2">/api/external-agents/<id>/heartbeat</code>.
|
|
264
|
+
</div>
|
|
265
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
266
|
+
{externalAgents.map((runtime) => (
|
|
267
|
+
<div key={runtime.id} className="p-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
268
|
+
<div className="flex items-center justify-between gap-3 mb-2">
|
|
269
|
+
<div className="min-w-0">
|
|
270
|
+
<div className="font-display text-[14px] font-600 text-text truncate">{runtime.name}</div>
|
|
271
|
+
<div className="text-[11px] text-text-3/60 truncate">{runtime.sourceType} · {runtime.transport || 'custom'}</div>
|
|
272
|
+
</div>
|
|
273
|
+
<span className={`text-[10px] font-700 px-2 py-0.5 rounded-[5px] uppercase tracking-wider ${
|
|
274
|
+
runtime.status === 'online'
|
|
275
|
+
? 'bg-emerald-400/10 text-emerald-300'
|
|
276
|
+
: runtime.status === 'stale'
|
|
277
|
+
? 'bg-amber-400/10 text-amber-300'
|
|
278
|
+
: 'bg-white/[0.04] text-text-3'
|
|
279
|
+
}`}>
|
|
280
|
+
{runtime.status}
|
|
281
|
+
</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div className="text-[12px] text-text-3/70">
|
|
284
|
+
{runtime.provider || 'No provider'}
|
|
285
|
+
{runtime.model ? ` · ${runtime.model}` : ''}
|
|
286
|
+
</div>
|
|
287
|
+
<div className="text-[11px] text-text-3/55 mt-2 font-mono truncate">{runtime.endpoint || runtime.workspace || runtime.id}</div>
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
{externalAgents.length === 0 && (
|
|
291
|
+
<div className="p-4 rounded-[14px] border border-dashed border-white/[0.08] text-[13px] text-text-3/70">
|
|
292
|
+
No external runtimes have registered yet.
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
142
298
|
</div>
|
|
143
299
|
)
|
|
144
300
|
}
|
|
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from '@/lib/provider-config'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
7
|
+
import { fetchProviderModelDiscovery } from '@/lib/provider-model-discovery-client'
|
|
7
8
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
9
|
import { toast } from 'sonner'
|
|
9
10
|
|
|
@@ -35,10 +36,10 @@ export function ProviderSheet() {
|
|
|
35
36
|
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
|
|
36
37
|
const [testMessage, setTestMessage] = useState('')
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
const [
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
39
|
+
const [liveModels, setLiveModels] = useState<string[]>([])
|
|
40
|
+
const [liveLoading, setLiveLoading] = useState(false)
|
|
41
|
+
const [liveMessage, setLiveMessage] = useState('')
|
|
42
|
+
const [liveCached, setLiveCached] = useState(false)
|
|
42
43
|
|
|
43
44
|
// Find editing provider in custom configs OR built-in list
|
|
44
45
|
const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
|
|
@@ -50,8 +51,9 @@ export function ProviderSheet() {
|
|
|
50
51
|
if (open) {
|
|
51
52
|
loadCredentials()
|
|
52
53
|
setNewModel('')
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
setLiveModels([])
|
|
55
|
+
setLiveMessage('')
|
|
56
|
+
setLiveCached(false)
|
|
55
57
|
setTestStatus('idle')
|
|
56
58
|
setTestMessage('')
|
|
57
59
|
if (editingCustom) {
|
|
@@ -79,28 +81,7 @@ export function ProviderSheet() {
|
|
|
79
81
|
setIsEnabled(true)
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
|
-
}, [open, editingId])
|
|
83
|
-
|
|
84
|
-
// Fetch local Ollama models when editing Ollama provider
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (!open || editingId !== 'ollama') return
|
|
87
|
-
setLocalLoading(true)
|
|
88
|
-
const endpoint = baseUrl || 'http://localhost:11434'
|
|
89
|
-
api<{ models: { name: string }[]; error?: string }>('GET', `/providers/ollama?endpoint=${encodeURIComponent(endpoint)}`)
|
|
90
|
-
.then((res) => {
|
|
91
|
-
if (res.error) {
|
|
92
|
-
setLocalError(res.error)
|
|
93
|
-
setLocalModels([])
|
|
94
|
-
} else {
|
|
95
|
-
setLocalModels(res.models.map((m) => m.name))
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
.catch(() => {
|
|
99
|
-
setLocalError('Failed to connect')
|
|
100
|
-
setLocalModels([])
|
|
101
|
-
})
|
|
102
|
-
.finally(() => setLocalLoading(false))
|
|
103
|
-
}, [open, editingId, baseUrl])
|
|
84
|
+
}, [open, editingId, credentials, editingBuiltin, editingCustom, loadCredentials])
|
|
104
85
|
|
|
105
86
|
// Reset test status when connection params change
|
|
106
87
|
useEffect(() => {
|
|
@@ -108,6 +89,12 @@ export function ProviderSheet() {
|
|
|
108
89
|
setTestMessage('')
|
|
109
90
|
}, [credentialId, baseUrl])
|
|
110
91
|
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
setLiveModels([])
|
|
94
|
+
setLiveMessage('')
|
|
95
|
+
setLiveCached(false)
|
|
96
|
+
}, [editingId, credentialId, baseUrl, requiresApiKey])
|
|
97
|
+
|
|
111
98
|
const handleTestConnection = async () => {
|
|
112
99
|
setTestStatus('testing')
|
|
113
100
|
setTestMessage('')
|
|
@@ -210,10 +197,43 @@ export function ProviderSheet() {
|
|
|
210
197
|
setModels(list.join(', '))
|
|
211
198
|
}
|
|
212
199
|
|
|
200
|
+
const handleLoadLiveModels = async (force = false) => {
|
|
201
|
+
if (!open) return
|
|
202
|
+
const providerId = editingId || 'custom'
|
|
203
|
+
setLiveLoading(true)
|
|
204
|
+
setLiveMessage('')
|
|
205
|
+
try {
|
|
206
|
+
const result = await fetchProviderModelDiscovery({
|
|
207
|
+
providerId,
|
|
208
|
+
credentialId,
|
|
209
|
+
endpoint: baseUrl,
|
|
210
|
+
force,
|
|
211
|
+
requiresApiKey: isBuiltin ? undefined : requiresApiKey,
|
|
212
|
+
})
|
|
213
|
+
setLiveModels(result.models)
|
|
214
|
+
setLiveCached(result.cached)
|
|
215
|
+
setLiveMessage(result.message || '')
|
|
216
|
+
if (!result.ok) {
|
|
217
|
+
toast.message(result.message || 'Live model discovery is unavailable.')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
setModels(result.models.join(', '))
|
|
221
|
+
toast.success(`Loaded ${result.models.length} live model${result.models.length === 1 ? '' : 's'}`)
|
|
222
|
+
} catch (err: unknown) {
|
|
223
|
+
const message = err instanceof Error ? err.message : 'Failed to load live models'
|
|
224
|
+
setLiveMessage(message)
|
|
225
|
+
toast.error(message)
|
|
226
|
+
} finally {
|
|
227
|
+
setLiveLoading(false)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
213
231
|
const credList = Object.values(credentials)
|
|
214
232
|
const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
215
|
-
const isNew = !editing
|
|
216
233
|
const showApiKey = isBuiltin ? editingBuiltin?.requiresApiKey || editingBuiltin?.optionalApiKey : requiresApiKey
|
|
234
|
+
const canDiscoverModels = isBuiltin
|
|
235
|
+
? Boolean(editingBuiltin?.supportsModelDiscovery)
|
|
236
|
+
: !!baseUrl.trim()
|
|
217
237
|
|
|
218
238
|
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"
|
|
219
239
|
|
|
@@ -254,26 +274,45 @@ export function ProviderSheet() {
|
|
|
254
274
|
<div className="mb-8">
|
|
255
275
|
<div className="flex items-center justify-between mb-3">
|
|
256
276
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Models</label>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
<div className="flex items-center gap-3">
|
|
278
|
+
{canDiscoverModels && (
|
|
279
|
+
<button
|
|
280
|
+
onClick={() => { void handleLoadLiveModels(Boolean(liveModels.length)) }}
|
|
281
|
+
disabled={liveLoading}
|
|
282
|
+
className="text-[11px] text-accent-bright hover:brightness-110 transition-colors cursor-pointer bg-transparent border-none disabled:opacity-50 disabled:cursor-default"
|
|
283
|
+
style={{ fontFamily: 'inherit' }}
|
|
284
|
+
>
|
|
285
|
+
{liveLoading ? 'Loading live models...' : liveModels.length > 0 ? 'Refresh live list' : 'Load live models'}
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
{isBuiltin && (
|
|
289
|
+
<button onClick={handleResetModels}
|
|
290
|
+
className="text-[11px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none"
|
|
291
|
+
style={{ fontFamily: 'inherit' }}>
|
|
292
|
+
Reset to defaults
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
264
296
|
</div>
|
|
265
297
|
|
|
298
|
+
{(liveMessage || liveCached) && (
|
|
299
|
+
<p className="text-[11px] text-text-3/70 mb-3">
|
|
300
|
+
{liveMessage}
|
|
301
|
+
{liveCached ? ' Cached.' : ''}
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
|
|
266
305
|
{isBuiltin ? (
|
|
267
306
|
<>
|
|
268
307
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
269
308
|
{modelList.map((model, i) => {
|
|
270
|
-
const
|
|
309
|
+
const isLive = liveModels.includes(model)
|
|
271
310
|
return (
|
|
272
311
|
<div key={`${model}-${i}`} className={`group/model flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border
|
|
273
|
-
${
|
|
312
|
+
${isLive ? 'bg-emerald-500/[0.08] border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'}`}>
|
|
274
313
|
<span className="text-[12px] text-text-2 font-mono">{model}</span>
|
|
275
|
-
{
|
|
276
|
-
<span className="text-[9px] font-600 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 text-emerald-400 uppercase tracking-wider">
|
|
314
|
+
{isLive && (
|
|
315
|
+
<span className="text-[9px] font-600 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 text-emerald-400 uppercase tracking-wider">live</span>
|
|
277
316
|
)}
|
|
278
317
|
<button
|
|
279
318
|
onClick={() => handleRemoveModel(i)}
|
|
@@ -288,34 +327,6 @@ export function ProviderSheet() {
|
|
|
288
327
|
})}
|
|
289
328
|
</div>
|
|
290
329
|
|
|
291
|
-
{/* Ollama: show available local models not yet in the list */}
|
|
292
|
-
{editingId === 'ollama' && !localLoading && localModels.length > 0 && (() => {
|
|
293
|
-
const missing = localModels.filter((m) => !modelList.includes(m))
|
|
294
|
-
if (missing.length === 0) return null
|
|
295
|
-
return (
|
|
296
|
-
<div className="mb-3">
|
|
297
|
-
<p className="text-[11px] text-text-3/60 mb-2">Available locally — click to add:</p>
|
|
298
|
-
<div className="flex flex-wrap gap-1.5">
|
|
299
|
-
{missing.map((m) => (
|
|
300
|
-
<button key={m} onClick={() => { setModels(models ? models + ', ' + m : m) }}
|
|
301
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] bg-emerald-500/[0.05] border border-emerald-500/15
|
|
302
|
-
hover:bg-emerald-500/10 transition-all cursor-pointer text-[12px] text-emerald-300/80 font-mono"
|
|
303
|
-
style={{ fontFamily: 'inherit' }}>
|
|
304
|
-
<span>+</span> {m}
|
|
305
|
-
</button>
|
|
306
|
-
))}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
)
|
|
310
|
-
})()}
|
|
311
|
-
|
|
312
|
-
{editingId === 'ollama' && localLoading && (
|
|
313
|
-
<p className="text-[11px] text-text-3/70 mb-3">Checking local Ollama instance...</p>
|
|
314
|
-
)}
|
|
315
|
-
{editingId === 'ollama' && localError && (
|
|
316
|
-
<p className="text-[11px] text-amber-400/60 mb-3">{localError}</p>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
330
|
<div className="flex gap-2">
|
|
320
331
|
<input
|
|
321
332
|
type="text"
|
|
@@ -431,13 +442,13 @@ export function ProviderSheet() {
|
|
|
431
442
|
onClick={async () => {
|
|
432
443
|
setSavingKey(true)
|
|
433
444
|
try {
|
|
434
|
-
const cred = await api<
|
|
445
|
+
const cred = await api<{ id: string }>('POST', '/credentials', { provider: editingId || name || 'custom', name: newKeyName.trim() || `${name || editingId || 'Custom'} key`, apiKey: newKeyValue.trim() })
|
|
435
446
|
await loadCredentials()
|
|
436
447
|
setCredentialId(cred.id)
|
|
437
448
|
setAddingKey(false)
|
|
438
449
|
setNewKeyName('')
|
|
439
450
|
setNewKeyValue('')
|
|
440
|
-
} catch (err:
|
|
451
|
+
} catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
|
|
441
452
|
finally { setSavingKey(false) }
|
|
442
453
|
}}
|
|
443
454
|
className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { XIcon } from 'lucide-react'
|
|
5
|
+
import { Dialog as DialogPrimitive } from 'radix-ui'
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
open: boolean
|
|
@@ -10,21 +12,35 @@ interface Props {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function BottomSheet({ open, onClose, children, wide }: Props) {
|
|
13
|
-
if (!open) return null
|
|
14
|
-
|
|
15
15
|
return (
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
<DialogPrimitive.Root open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
|
|
17
|
+
<DialogPrimitive.Portal>
|
|
18
|
+
<DialogPrimitive.Overlay
|
|
19
|
+
className="fixed inset-0 z-100 bg-black/72 backdrop-blur-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
20
|
+
/>
|
|
21
|
+
<DialogPrimitive.Content
|
|
22
|
+
className={`fixed inset-x-0 bottom-0 z-100 mx-auto flex max-h-[92vh] w-full flex-col bg-raised shadow-[0_24px_80px_rgba(0,0,0,0.6),0_0_1px_rgba(255,255,255,0.05)] outline-none
|
|
23
|
+
rounded-t-[24px] border border-white/[0.06]
|
|
24
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom
|
|
25
|
+
sm:inset-x-auto sm:top-[50%] sm:bottom-auto sm:left-[50%] sm:w-[calc(100%-2rem)] sm:translate-x-[-50%] sm:translate-y-[-50%] sm:rounded-[24px]
|
|
26
|
+
sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95
|
|
27
|
+
${wide ? 'sm:max-w-[760px]' : 'sm:max-w-[560px]'}`}
|
|
28
|
+
style={{ animationDuration: '220ms' }}
|
|
29
|
+
>
|
|
30
|
+
<div className="relative shrink-0 px-4 pt-3 sm:px-5 sm:pt-5">
|
|
31
|
+
<div className="mx-auto h-1 w-10 rounded-full bg-white/[0.08] sm:hidden" />
|
|
32
|
+
<DialogPrimitive.Close
|
|
33
|
+
className="absolute right-3 top-2.5 inline-flex h-9 w-9 items-center justify-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] text-text-3 transition-all hover:bg-white/[0.06] hover:text-text-2 focus:outline-none focus:ring-2 focus:ring-accent-bright/30 sm:right-4 sm:top-4"
|
|
34
|
+
>
|
|
35
|
+
<XIcon className="size-4" />
|
|
36
|
+
<span className="sr-only">Close</span>
|
|
37
|
+
</DialogPrimitive.Close>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex-1 overflow-y-auto px-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-3 sm:px-8 sm:pb-8 sm:pt-5">
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
</DialogPrimitive.Content>
|
|
43
|
+
</DialogPrimitive.Portal>
|
|
44
|
+
</DialogPrimitive.Root>
|
|
29
45
|
)
|
|
30
46
|
}
|