@swarmclawai/swarmclaw 0.7.2 → 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 +116 -50
- 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 +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- 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]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- 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/files/open/route.ts +16 -14
- 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/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- 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 +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- 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 +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- 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/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -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-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- 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/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- 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 +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- 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/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -11,7 +11,7 @@ import type { PluginMeta, PluginSettingsField, MarketplacePlugin } from '@/types
|
|
|
11
11
|
function pluginDescription(plugin: PluginMeta): string {
|
|
12
12
|
const raw = (plugin.description || '').trim()
|
|
13
13
|
if (raw) return raw
|
|
14
|
-
const sourceLabel = plugin.
|
|
14
|
+
const sourceLabel = plugin.isBuiltin ? 'core plugin' : 'installed plugin'
|
|
15
15
|
return `No description provided. This ${sourceLabel} is available and can be configured from this panel.`
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -22,6 +22,7 @@ function pluginCapabilityBadges(plugin: PluginMeta): string[] {
|
|
|
22
22
|
if (plugin.hasUI) badges.push('UI extension')
|
|
23
23
|
if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
|
|
24
24
|
if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
|
|
25
|
+
if (plugin.hasDependencyManifest) badges.push(`${plugin.dependencyCount ?? 0} dep${plugin.dependencyCount === 1 ? '' : 's'}`)
|
|
25
26
|
return badges
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -46,8 +47,10 @@ export function PluginSheet() {
|
|
|
46
47
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
47
48
|
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
48
49
|
const [pluginSettingsValues, setPluginSettingsValues] = useState<Record<string, unknown>>({})
|
|
50
|
+
const [configuredSecretFields, setConfiguredSecretFields] = useState<string[]>([])
|
|
49
51
|
const [pluginSettingsLoading, setPluginSettingsLoading] = useState(false)
|
|
50
52
|
const [pluginSettingsSaving, setPluginSettingsSaving] = useState(false)
|
|
53
|
+
const [dependencyInstalling, setDependencyInstalling] = useState(false)
|
|
51
54
|
|
|
52
55
|
const editing = editingFilename ? plugins[editingFilename] : null
|
|
53
56
|
|
|
@@ -55,12 +58,19 @@ export function PluginSheet() {
|
|
|
55
58
|
useEffect(() => {
|
|
56
59
|
if (!editing?.settingsFields?.length) {
|
|
57
60
|
setPluginSettingsValues({})
|
|
61
|
+
setConfiguredSecretFields([])
|
|
58
62
|
return
|
|
59
63
|
}
|
|
60
64
|
setPluginSettingsLoading(true)
|
|
61
|
-
api<Record<string, unknown
|
|
62
|
-
.then((data) =>
|
|
63
|
-
|
|
65
|
+
api<{ values?: Record<string, unknown>; configuredSecretFields?: string[] }>('GET', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`)
|
|
66
|
+
.then((data) => {
|
|
67
|
+
setPluginSettingsValues(data?.values ?? {})
|
|
68
|
+
setConfiguredSecretFields(Array.isArray(data?.configuredSecretFields) ? data.configuredSecretFields : [])
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
setPluginSettingsValues({})
|
|
72
|
+
setConfiguredSecretFields([])
|
|
73
|
+
})
|
|
64
74
|
.finally(() => setPluginSettingsLoading(false))
|
|
65
75
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
76
|
}, [editingFilename])
|
|
@@ -69,7 +79,24 @@ export function PluginSheet() {
|
|
|
69
79
|
if (!editing) return
|
|
70
80
|
setPluginSettingsSaving(true)
|
|
71
81
|
try {
|
|
72
|
-
|
|
82
|
+
const secretFieldSet = new Set(
|
|
83
|
+
(editing.settingsFields || [])
|
|
84
|
+
.filter((field) => field.type === 'secret')
|
|
85
|
+
.map((field) => field.key),
|
|
86
|
+
)
|
|
87
|
+
const payload = Object.fromEntries(
|
|
88
|
+
Object.entries(pluginSettingsValues).filter(([key, value]) => {
|
|
89
|
+
if (!secretFieldSet.has(key)) return true
|
|
90
|
+
return value !== undefined && value !== ''
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
const response = await api<{ values?: Record<string, unknown>; configuredSecretFields?: string[] }>(
|
|
94
|
+
'PUT',
|
|
95
|
+
`/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`,
|
|
96
|
+
payload,
|
|
97
|
+
)
|
|
98
|
+
setPluginSettingsValues(response?.values ?? {})
|
|
99
|
+
setConfiguredSecretFields(Array.isArray(response?.configuredSecretFields) ? response.configuredSecretFields : [])
|
|
73
100
|
toast.success('Plugin settings saved')
|
|
74
101
|
} catch (err: unknown) {
|
|
75
102
|
toast.error(err instanceof Error ? err.message : 'Failed to save settings')
|
|
@@ -126,6 +153,24 @@ export function PluginSheet() {
|
|
|
126
153
|
setDeleting(false)
|
|
127
154
|
}
|
|
128
155
|
|
|
156
|
+
const installDependencies = useCallback(async () => {
|
|
157
|
+
if (!editing?.filename) return
|
|
158
|
+
setDependencyInstalling(true)
|
|
159
|
+
try {
|
|
160
|
+
const response = await api<{ ok: boolean }>('POST', '/plugins/dependencies', {
|
|
161
|
+
filename: editing.filename,
|
|
162
|
+
packageManager: editing.packageManager,
|
|
163
|
+
})
|
|
164
|
+
if (response?.ok) {
|
|
165
|
+
await loadPlugins()
|
|
166
|
+
toast.success('Plugin dependencies installed')
|
|
167
|
+
}
|
|
168
|
+
} catch (err: unknown) {
|
|
169
|
+
toast.error(err instanceof Error ? err.message : 'Failed to install plugin dependencies')
|
|
170
|
+
}
|
|
171
|
+
setDependencyInstalling(false)
|
|
172
|
+
}, [editing?.filename, editing?.packageManager, loadPlugins])
|
|
173
|
+
|
|
129
174
|
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
130
175
|
setInstalling(p.id)
|
|
131
176
|
const toastId = toast.loading(`Installing ${p.name}...`)
|
|
@@ -189,7 +234,9 @@ export function PluginSheet() {
|
|
|
189
234
|
<div className="grid grid-cols-2 gap-2 mt-3">
|
|
190
235
|
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
191
236
|
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Source</div>
|
|
192
|
-
<div className="text-[11px] text-text-2">
|
|
237
|
+
<div className="text-[11px] text-text-2">
|
|
238
|
+
{editing.isBuiltin ? 'Core Platform' : editing.source === 'marketplace' ? 'Marketplace Extension' : 'Local Extension'}
|
|
239
|
+
</div>
|
|
193
240
|
</div>
|
|
194
241
|
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
195
242
|
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Author</div>
|
|
@@ -219,6 +266,54 @@ export function PluginSheet() {
|
|
|
219
266
|
)}
|
|
220
267
|
</div>
|
|
221
268
|
|
|
269
|
+
{(editing.hasDependencyManifest || !editing.isBuiltin) && (
|
|
270
|
+
<div className="py-4 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
271
|
+
<div className="flex items-start justify-between gap-3">
|
|
272
|
+
<div>
|
|
273
|
+
<div className="text-[13px] font-600 text-text">Dependencies</div>
|
|
274
|
+
<p className="text-[11px] text-text-3/60 mt-1">
|
|
275
|
+
{editing.hasDependencyManifest
|
|
276
|
+
? `Managed in a per-plugin workspace${editing.packageManager ? ` via ${editing.packageManager}` : ''}.`
|
|
277
|
+
: 'No package.json manifest is currently attached to this plugin.'}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
{editing.hasDependencyManifest && (
|
|
281
|
+
<button
|
|
282
|
+
onClick={() => { void installDependencies() }}
|
|
283
|
+
disabled={dependencyInstalling}
|
|
284
|
+
className="px-3 py-2 rounded-[10px] bg-accent-soft text-[11px] font-700 text-accent-bright hover:bg-accent-bright/15 transition-all cursor-pointer border-none disabled:opacity-50"
|
|
285
|
+
style={{ fontFamily: 'inherit' }}
|
|
286
|
+
>
|
|
287
|
+
{dependencyInstalling ? 'Installing…' : 'Install / Refresh'}
|
|
288
|
+
</button>
|
|
289
|
+
)}
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div className="grid grid-cols-2 gap-2 mt-3">
|
|
293
|
+
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
294
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Runtime deps</div>
|
|
295
|
+
<div className="text-[11px] text-text-2">{editing.dependencyCount ?? 0}</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
298
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Status</div>
|
|
299
|
+
<div className="text-[11px] text-text-2">{editing.dependencyInstallStatus || 'none'}</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{editing.dependencyInstallError && (
|
|
304
|
+
<div className="mt-3 p-2.5 rounded-[10px] bg-red-500/[0.08] border border-red-500/20 text-[11px] text-red-300/90">
|
|
305
|
+
{editing.dependencyInstallError}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{editing.dependencyInstalledAt && (
|
|
310
|
+
<p className="text-[10px] text-text-3/45 mt-3">
|
|
311
|
+
Last installed {new Date(editing.dependencyInstalledAt).toLocaleString()}
|
|
312
|
+
</p>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
222
317
|
<div className="flex items-center justify-between py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
223
318
|
<div>
|
|
224
319
|
<span className="text-[13px] font-600 text-text block">Enabled</span>
|
|
@@ -246,6 +341,7 @@ export function PluginSheet() {
|
|
|
246
341
|
key={field.key}
|
|
247
342
|
field={field}
|
|
248
343
|
value={pluginSettingsValues[field.key]}
|
|
344
|
+
configured={configuredSecretFields.includes(field.key)}
|
|
249
345
|
onChange={(v) => setPluginSettingsValues((prev) => ({ ...prev, [field.key]: v }))}
|
|
250
346
|
/>
|
|
251
347
|
))}
|
|
@@ -442,7 +538,7 @@ export function PluginSheet() {
|
|
|
442
538
|
</p>
|
|
443
539
|
)}
|
|
444
540
|
<p className="text-[10px] text-text-3/60 mt-3">
|
|
445
|
-
Works with SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
|
|
541
|
+
Works with `.js` / `.mjs` SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
|
|
446
542
|
</p>
|
|
447
543
|
</div>
|
|
448
544
|
)}
|
|
@@ -465,7 +561,17 @@ export function PluginSheet() {
|
|
|
465
561
|
)
|
|
466
562
|
}
|
|
467
563
|
|
|
468
|
-
function PluginSettingRow({
|
|
564
|
+
function PluginSettingRow({
|
|
565
|
+
field,
|
|
566
|
+
value,
|
|
567
|
+
configured,
|
|
568
|
+
onChange,
|
|
569
|
+
}: {
|
|
570
|
+
field: PluginSettingsField
|
|
571
|
+
value: unknown
|
|
572
|
+
configured: boolean
|
|
573
|
+
onChange: (v: unknown) => void
|
|
574
|
+
}) {
|
|
469
575
|
const inputCls = 'w-full py-2 px-3 rounded-[8px] text-[12px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30'
|
|
470
576
|
|
|
471
577
|
return (
|
|
@@ -509,11 +615,14 @@ function PluginSettingRow({ field, value, onChange }: { field: PluginSettingsFie
|
|
|
509
615
|
type={field.type === 'secret' ? 'password' : 'text'}
|
|
510
616
|
value={String(value ?? field.defaultValue ?? '')}
|
|
511
617
|
onChange={(e) => onChange(e.target.value || undefined)}
|
|
512
|
-
placeholder={field.placeholder}
|
|
618
|
+
placeholder={field.type === 'secret' && configured ? 'Stored securely. Enter a new value to replace it.' : field.placeholder}
|
|
513
619
|
className={inputCls}
|
|
514
620
|
style={{ fontFamily: 'inherit' }}
|
|
515
621
|
/>
|
|
516
622
|
)}
|
|
623
|
+
{field.type === 'secret' && configured && (
|
|
624
|
+
<p className="text-[10px] text-emerald-400/90 mt-1">Stored securely. Leave blank to keep the current value.</p>
|
|
625
|
+
)}
|
|
517
626
|
{field.help && <p className="text-[10px] text-text-3/60 mt-1">{field.help}</p>}
|
|
518
627
|
</div>
|
|
519
628
|
)
|
|
@@ -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,6 +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, setNow] = useState(() => Date.now())
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
109
|
+
return () => window.clearInterval(intervalId)
|
|
110
|
+
}, [])
|
|
105
111
|
|
|
106
112
|
const project = activeProjectFilter ? projects[activeProjectFilter] : null
|
|
107
113
|
|
|
@@ -150,6 +156,93 @@ export function ProjectDetail() {
|
|
|
150
156
|
return items.sort((a, b) => b.time - a.time).slice(0, 12)
|
|
151
157
|
}, [projectTasks, projectSchedules, projectAgents])
|
|
152
158
|
|
|
159
|
+
const approvalTasks = useMemo(
|
|
160
|
+
() => projectTasks.filter((task) => !!task.pendingApproval),
|
|
161
|
+
[projectTasks],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const blockedTasks = useMemo(
|
|
165
|
+
() => projectTasks.filter((task) => (task.blockedBy?.length || 0) > 0),
|
|
166
|
+
[projectTasks],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const overdueTasks = useMemo(
|
|
170
|
+
() => projectTasks.filter((task) => !!task.dueAt && task.dueAt < now && task.status !== 'completed' && task.status !== 'archived'),
|
|
171
|
+
[now, projectTasks],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const staleTasks = useMemo(
|
|
175
|
+
() => projectTasks.filter((task) => task.status !== 'completed' && task.status !== 'archived' && now - task.updatedAt > 3 * 24 * 60 * 60 * 1000),
|
|
176
|
+
[now, projectTasks],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const overdueSchedules = useMemo(
|
|
180
|
+
() => projectSchedules.filter((schedule) => schedule.status === 'active' && !!schedule.nextRunAt && schedule.nextRunAt < now),
|
|
181
|
+
[now, projectSchedules],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const nextScheduledRun = useMemo(
|
|
185
|
+
() => projectSchedules
|
|
186
|
+
.filter((schedule) => schedule.status === 'active' && !!schedule.nextRunAt && schedule.nextRunAt >= now)
|
|
187
|
+
.sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0))[0] || null,
|
|
188
|
+
[now, projectSchedules],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const busiestAgent = useMemo(() => {
|
|
192
|
+
return projectAgents
|
|
193
|
+
.map((agent) => ({
|
|
194
|
+
agent,
|
|
195
|
+
taskCount: projectTasks.filter((task) => task.agentId === agent.id && task.status !== 'completed' && task.status !== 'archived').length,
|
|
196
|
+
}))
|
|
197
|
+
.sort((a, b) => b.taskCount - a.taskCount)[0] || null
|
|
198
|
+
}, [projectAgents, projectTasks])
|
|
199
|
+
|
|
200
|
+
const attentionItems = useMemo(() => {
|
|
201
|
+
const seen = new Set<string>()
|
|
202
|
+
const items: Array<{ id: string; label: string; detail: string; tone: string; onClick: () => void }> = []
|
|
203
|
+
for (const task of approvalTasks.slice(0, 2)) {
|
|
204
|
+
seen.add(task.id)
|
|
205
|
+
items.push({
|
|
206
|
+
id: `approval-${task.id}`,
|
|
207
|
+
label: task.title,
|
|
208
|
+
detail: 'Awaiting tool approval',
|
|
209
|
+
tone: 'text-amber-400',
|
|
210
|
+
onClick: () => { setEditingTaskId(task.id); setTaskSheetOpen(true) },
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
for (const task of blockedTasks.slice(0, 2)) {
|
|
214
|
+
if (seen.has(task.id)) continue
|
|
215
|
+
seen.add(task.id)
|
|
216
|
+
items.push({
|
|
217
|
+
id: `blocked-${task.id}`,
|
|
218
|
+
label: task.title,
|
|
219
|
+
detail: `${task.blockedBy?.length || 0} blocker${task.blockedBy?.length === 1 ? '' : 's'}`,
|
|
220
|
+
tone: 'text-rose-400',
|
|
221
|
+
onClick: () => { setEditingTaskId(task.id); setTaskSheetOpen(true) },
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
if (staleTasks[0] && !seen.has(staleTasks[0].id)) {
|
|
225
|
+
seen.add(staleTasks[0].id)
|
|
226
|
+
items.push({
|
|
227
|
+
id: `stale-${staleTasks[0].id}`,
|
|
228
|
+
label: staleTasks[0].title,
|
|
229
|
+
detail: 'No recent progress in 3+ days',
|
|
230
|
+
tone: 'text-sky-400',
|
|
231
|
+
onClick: () => { setEditingTaskId(staleTasks[0].id); setTaskSheetOpen(true) },
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
if (overdueSchedules[0]) {
|
|
235
|
+
items.push({
|
|
236
|
+
id: `schedule-${overdueSchedules[0].id}`,
|
|
237
|
+
label: overdueSchedules[0].name,
|
|
238
|
+
detail: 'Schedule missed its next run',
|
|
239
|
+
tone: 'text-red-400',
|
|
240
|
+
onClick: () => { setEditingScheduleId(overdueSchedules[0].id); setScheduleSheetOpen(true) },
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return items.slice(0, 5)
|
|
244
|
+
}, [approvalTasks, blockedTasks, overdueSchedules, setEditingScheduleId, setEditingTaskId, setScheduleSheetOpen, setTaskSheetOpen, staleTasks])
|
|
245
|
+
|
|
153
246
|
const handleUnassignAgent = async (agentId: string) => {
|
|
154
247
|
await updateAgent(agentId, { projectId: undefined })
|
|
155
248
|
await loadAgents()
|
|
@@ -212,6 +305,101 @@ export function ProjectDetail() {
|
|
|
212
305
|
</div>
|
|
213
306
|
</div>
|
|
214
307
|
|
|
308
|
+
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_280px] gap-4 mb-8">
|
|
309
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
|
|
310
|
+
<div className="flex items-center justify-between gap-4 mb-4">
|
|
311
|
+
<div>
|
|
312
|
+
<h2 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">Project Health</h2>
|
|
313
|
+
<p className="text-[12px] text-text-3/60 mt-1">Surface blockers and the next useful action before digging into the full history.</p>
|
|
314
|
+
</div>
|
|
315
|
+
<button
|
|
316
|
+
onClick={() => setActiveView('tasks')}
|
|
317
|
+
className="shrink-0 px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
|
|
318
|
+
style={{ fontFamily: 'inherit' }}
|
|
319
|
+
>
|
|
320
|
+
Open Task Board
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
325
|
+
{[
|
|
326
|
+
{ label: 'Awaiting Approval', value: approvalTasks.length, tone: 'text-amber-400', hint: 'Tasks paused on approval' },
|
|
327
|
+
{ label: 'Blocked', value: blockedTasks.length, tone: 'text-rose-400', hint: 'Tasks waiting on dependencies' },
|
|
328
|
+
{ label: 'Overdue', value: overdueTasks.length + overdueSchedules.length, tone: 'text-red-400', hint: 'Tasks or schedules behind plan' },
|
|
329
|
+
{ label: 'Stale Tasks', value: staleTasks.length, tone: 'text-sky-400', hint: 'No meaningful progress in 3+ days' },
|
|
330
|
+
].map((item) => (
|
|
331
|
+
<div key={item.label} className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3">
|
|
332
|
+
<div className={`text-[22px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
|
|
333
|
+
<div className="text-[11px] font-600 text-text-2 mt-0.5">{item.label}</div>
|
|
334
|
+
<p className="text-[10px] text-text-3/45 mt-1 leading-relaxed">{item.hint}</p>
|
|
335
|
+
</div>
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="flex flex-wrap gap-2 mt-4">
|
|
340
|
+
{busiestAgent?.agent && (
|
|
341
|
+
<button
|
|
342
|
+
onClick={() => { setCurrentAgent(busiestAgent.agent.id); setActiveView('agents') }}
|
|
343
|
+
className="inline-flex items-center gap-2 px-3 py-2 rounded-[10px] bg-accent-soft text-[12px] font-600 text-accent-bright hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
|
|
344
|
+
style={{ fontFamily: 'inherit' }}
|
|
345
|
+
>
|
|
346
|
+
<AgentAvatar seed={busiestAgent.agent.avatarSeed} avatarUrl={busiestAgent.agent.avatarUrl} name={busiestAgent.agent.name} size={18} />
|
|
347
|
+
Open busiest agent
|
|
348
|
+
</button>
|
|
349
|
+
)}
|
|
350
|
+
{(approvalTasks[0] || blockedTasks[0] || overdueTasks[0]) && (
|
|
351
|
+
<button
|
|
352
|
+
onClick={() => {
|
|
353
|
+
const nextTask = approvalTasks[0] || blockedTasks[0] || overdueTasks[0]
|
|
354
|
+
if (!nextTask) return
|
|
355
|
+
setEditingTaskId(nextTask.id)
|
|
356
|
+
setTaskSheetOpen(true)
|
|
357
|
+
}}
|
|
358
|
+
className="px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
|
|
359
|
+
style={{ fontFamily: 'inherit' }}
|
|
360
|
+
>
|
|
361
|
+
Review next task
|
|
362
|
+
</button>
|
|
363
|
+
)}
|
|
364
|
+
{nextScheduledRun && (
|
|
365
|
+
<button
|
|
366
|
+
onClick={() => { setEditingScheduleId(nextScheduledRun.id); setScheduleSheetOpen(true) }}
|
|
367
|
+
className="px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
|
|
368
|
+
style={{ fontFamily: 'inherit' }}
|
|
369
|
+
>
|
|
370
|
+
Next run {relativeDate(nextScheduledRun.nextRunAt || now)}
|
|
371
|
+
</button>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
|
|
377
|
+
<div className="flex items-center justify-between mb-3">
|
|
378
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Needs Attention</h3>
|
|
379
|
+
<span className="text-[11px] text-text-3/40">{attentionItems.length || 0} items</span>
|
|
380
|
+
</div>
|
|
381
|
+
{attentionItems.length === 0 ? (
|
|
382
|
+
<div className="rounded-[12px] border border-dashed border-white/[0.08] px-4 py-6 text-center">
|
|
383
|
+
<p className="text-[12px] text-text-3/45">No urgent blockers right now.</p>
|
|
384
|
+
</div>
|
|
385
|
+
) : (
|
|
386
|
+
<div className="flex flex-col gap-2">
|
|
387
|
+
{attentionItems.map((item) => (
|
|
388
|
+
<button
|
|
389
|
+
key={item.id}
|
|
390
|
+
onClick={item.onClick}
|
|
391
|
+
className="w-full rounded-[12px] border border-white/[0.06] bg-surface/60 px-3.5 py-3 text-left hover:bg-white/[0.04] transition-all cursor-pointer"
|
|
392
|
+
style={{ fontFamily: 'inherit' }}
|
|
393
|
+
>
|
|
394
|
+
<div className={`text-[11px] font-700 uppercase tracking-[0.08em] ${item.tone}`}>{item.detail}</div>
|
|
395
|
+
<div className="text-[13px] font-600 text-text mt-1 line-clamp-2">{item.label}</div>
|
|
396
|
+
</button>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
215
403
|
{/* Stats cards */}
|
|
216
404
|
<div className="grid grid-cols-4 gap-3 mb-8">
|
|
217
405
|
{[
|
|
@@ -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
|
}
|