@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useMemo } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
|
|
6
|
+
interface DirEntry {
|
|
7
|
+
name: string
|
|
8
|
+
path: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DirApiResponse {
|
|
12
|
+
dirs: DirEntry[]
|
|
13
|
+
currentPath: string
|
|
14
|
+
parentPath: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DirBrowserProps {
|
|
18
|
+
value: string | null
|
|
19
|
+
file?: string | null
|
|
20
|
+
onChange: (dir: string, file?: string | null) => void
|
|
21
|
+
onClear: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Mode = 'native' | 'browse'
|
|
25
|
+
|
|
26
|
+
export function DirBrowser({ value, file, onChange, onClear }: DirBrowserProps) {
|
|
27
|
+
const [mode, setMode] = useState<Mode>('native')
|
|
28
|
+
const [picking, setPicking] = useState<'file' | 'folder' | null>(null)
|
|
29
|
+
|
|
30
|
+
// Browse mode state
|
|
31
|
+
const [browsePath, setBrowsePath] = useState('~/Dev')
|
|
32
|
+
const [dirs, setDirs] = useState<DirEntry[]>([])
|
|
33
|
+
const [currentPath, setCurrentPath] = useState('')
|
|
34
|
+
const [parentPath, setParentPath] = useState<string | null>(null)
|
|
35
|
+
const [loading, setLoading] = useState(false)
|
|
36
|
+
const [pathInput, setPathInput] = useState('')
|
|
37
|
+
const [search, setSearch] = useState('')
|
|
38
|
+
|
|
39
|
+
const fetchDirs = useCallback(async (dirPath: string) => {
|
|
40
|
+
setLoading(true)
|
|
41
|
+
try {
|
|
42
|
+
const data = await api<DirApiResponse>('GET', `/dirs?path=${encodeURIComponent(dirPath)}`)
|
|
43
|
+
setDirs(data.dirs || [])
|
|
44
|
+
setCurrentPath(data.currentPath || dirPath)
|
|
45
|
+
setParentPath(data.parentPath || null)
|
|
46
|
+
setPathInput(data.currentPath || dirPath)
|
|
47
|
+
} catch {
|
|
48
|
+
setDirs([])
|
|
49
|
+
}
|
|
50
|
+
setLoading(false)
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (mode === 'browse') {
|
|
55
|
+
fetchDirs(browsePath)
|
|
56
|
+
setSearch('')
|
|
57
|
+
}
|
|
58
|
+
}, [browsePath, mode, fetchDirs])
|
|
59
|
+
|
|
60
|
+
const filteredDirs = useMemo(() => {
|
|
61
|
+
if (!search) return dirs
|
|
62
|
+
const q = search.toLowerCase()
|
|
63
|
+
return dirs.filter((d) => d.name.toLowerCase().includes(q))
|
|
64
|
+
}, [dirs, search])
|
|
65
|
+
|
|
66
|
+
const navigateTo = (path: string) => setBrowsePath(path)
|
|
67
|
+
|
|
68
|
+
const handlePathSubmit = (e: React.KeyboardEvent) => {
|
|
69
|
+
if (e.key === 'Enter' && pathInput.trim()) {
|
|
70
|
+
navigateTo(pathInput.trim())
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handlePick = async (pickMode: 'file' | 'folder') => {
|
|
75
|
+
setPicking(pickMode)
|
|
76
|
+
try {
|
|
77
|
+
const data = await api<{ directory: string | null; file: string | null }>('POST', '/dirs/pick', { mode: pickMode })
|
|
78
|
+
if (data.directory) {
|
|
79
|
+
onChange(data.directory, data.file)
|
|
80
|
+
}
|
|
81
|
+
} catch { /* cancelled or error */ }
|
|
82
|
+
setPicking(null)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Breadcrumbs for browse mode
|
|
86
|
+
const homedir = currentPath.match(/^\/Users\/[^/]+/)?.[0] || ''
|
|
87
|
+
const breadcrumbs: Array<{ label: string; path: string }> = []
|
|
88
|
+
if (currentPath && homedir) {
|
|
89
|
+
const relative = currentPath.slice(homedir.length)
|
|
90
|
+
const parts = relative.split('/').filter(Boolean)
|
|
91
|
+
breadcrumbs.push({ label: '~', path: homedir })
|
|
92
|
+
let acc = homedir
|
|
93
|
+
for (const p of parts) {
|
|
94
|
+
acc = `${acc}/${p}`
|
|
95
|
+
breadcrumbs.push({ label: p, path: acc })
|
|
96
|
+
}
|
|
97
|
+
} else if (currentPath) {
|
|
98
|
+
const parts = currentPath.split('/').filter(Boolean)
|
|
99
|
+
breadcrumbs.push({ label: '/', path: '/' })
|
|
100
|
+
let acc = ''
|
|
101
|
+
for (const p of parts) {
|
|
102
|
+
acc = `${acc}/${p}`
|
|
103
|
+
breadcrumbs.push({ label: p, path: acc })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Selected state
|
|
108
|
+
if (value) {
|
|
109
|
+
const displayDir = value.replace(/^\/Users\/\w+/, '~')
|
|
110
|
+
const displayFile = file ? file.split('/').pop() : null
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<div className="flex-1 min-w-0 px-4 py-3 rounded-[14px] border border-accent-bright/20 bg-accent-soft overflow-hidden">
|
|
114
|
+
<div className="text-accent-bright text-[14px] font-mono truncate">{displayDir}</div>
|
|
115
|
+
{displayFile && (
|
|
116
|
+
<div className="text-accent-bright/60 text-[12px] font-mono truncate mt-0.5">
|
|
117
|
+
{displayFile}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
<button
|
|
122
|
+
onClick={onClear}
|
|
123
|
+
className="shrink-0 px-3 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text-3 text-[13px] cursor-pointer hover:bg-surface-2 transition-colors"
|
|
124
|
+
style={{ fontFamily: 'inherit' }}
|
|
125
|
+
>
|
|
126
|
+
Clear
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="space-y-3">
|
|
134
|
+
{mode === 'native' ? (
|
|
135
|
+
<>
|
|
136
|
+
{/* Native picker buttons */}
|
|
137
|
+
<div className="flex gap-3">
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => handlePick('folder')}
|
|
140
|
+
disabled={picking !== null}
|
|
141
|
+
className="flex-1 flex items-center justify-center gap-2.5 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 hover:border-white/[0.12] transition-all disabled:opacity-40"
|
|
142
|
+
style={{ fontFamily: 'inherit' }}
|
|
143
|
+
>
|
|
144
|
+
{picking === 'folder' ? (
|
|
145
|
+
<span className="text-text-3">Opening...</span>
|
|
146
|
+
) : (
|
|
147
|
+
<>
|
|
148
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" className="text-text-3">
|
|
149
|
+
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2Z" />
|
|
150
|
+
</svg>
|
|
151
|
+
Choose Folder
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</button>
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => handlePick('file')}
|
|
157
|
+
disabled={picking !== null}
|
|
158
|
+
className="flex-1 flex items-center justify-center gap-2.5 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 hover:border-white/[0.12] transition-all disabled:opacity-40"
|
|
159
|
+
style={{ fontFamily: 'inherit' }}
|
|
160
|
+
>
|
|
161
|
+
{picking === 'file' ? (
|
|
162
|
+
<span className="text-text-3">Opening...</span>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
166
|
+
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
167
|
+
<polyline points="14 2 14 8 20 8" />
|
|
168
|
+
</svg>
|
|
169
|
+
Choose File
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => setMode('browse')}
|
|
176
|
+
className="text-[12px] text-text-3/60 hover:text-text-3 transition-colors cursor-pointer bg-transparent border-none p-0"
|
|
177
|
+
>
|
|
178
|
+
Or browse directories manually
|
|
179
|
+
</button>
|
|
180
|
+
</>
|
|
181
|
+
) : (
|
|
182
|
+
<>
|
|
183
|
+
{/* Path input */}
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
value={pathInput}
|
|
187
|
+
onChange={(e) => setPathInput(e.target.value)}
|
|
188
|
+
onKeyDown={handlePathSubmit}
|
|
189
|
+
placeholder="Type a path and press Enter..."
|
|
190
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] font-mono outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
191
|
+
/>
|
|
192
|
+
|
|
193
|
+
{/* Breadcrumb bar */}
|
|
194
|
+
<div className="flex items-center gap-1 px-1 overflow-x-auto scrollbar-none">
|
|
195
|
+
{parentPath && (
|
|
196
|
+
<button
|
|
197
|
+
onClick={() => navigateTo(parentPath)}
|
|
198
|
+
className="shrink-0 w-7 h-7 rounded-[8px] border border-white/[0.06] bg-surface text-text-3 text-[13px] cursor-pointer hover:bg-surface-2 hover:text-text-2 transition-colors flex items-center justify-center"
|
|
199
|
+
>
|
|
200
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
201
|
+
<polyline points="15 18 9 12 15 6" />
|
|
202
|
+
</svg>
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
{breadcrumbs.map((bc, i) => (
|
|
206
|
+
<span key={bc.path} className="flex items-center shrink-0">
|
|
207
|
+
{i > 0 && <span className="text-text-3/60 text-[12px] mx-0.5">/</span>}
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => navigateTo(bc.path)}
|
|
210
|
+
className={`px-2 py-1 rounded-[6px] text-[12px] font-600 cursor-pointer transition-colors
|
|
211
|
+
${i === breadcrumbs.length - 1
|
|
212
|
+
? 'text-text bg-white/[0.04]'
|
|
213
|
+
: 'text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
|
|
214
|
+
>
|
|
215
|
+
{bc.label}
|
|
216
|
+
</button>
|
|
217
|
+
</span>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Search filter */}
|
|
222
|
+
{dirs.length > 5 && (
|
|
223
|
+
<div className="relative">
|
|
224
|
+
<svg className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-3/70" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
225
|
+
<circle cx="11" cy="11" r="8" />
|
|
226
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
227
|
+
</svg>
|
|
228
|
+
<input
|
|
229
|
+
type="text"
|
|
230
|
+
value={search}
|
|
231
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
232
|
+
placeholder="Filter directories..."
|
|
233
|
+
className="w-full pl-9 pr-4 py-2.5 rounded-[12px] border border-white/[0.06] bg-surface-2 text-text text-[13px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-white/[0.12]"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Directory list */}
|
|
239
|
+
<div className="max-h-[200px] overflow-y-auto rounded-[14px] border border-white/[0.06] bg-surface divide-y divide-white/[0.04]">
|
|
240
|
+
{loading ? (
|
|
241
|
+
<div className="py-8 text-center text-[13px] text-text-3/50">Loading...</div>
|
|
242
|
+
) : filteredDirs.length === 0 ? (
|
|
243
|
+
<div className="py-8 text-center text-[13px] text-text-3/50">
|
|
244
|
+
{search ? 'No matching directories' : 'No subdirectories'}
|
|
245
|
+
</div>
|
|
246
|
+
) : (
|
|
247
|
+
filteredDirs.map((d) => (
|
|
248
|
+
<button
|
|
249
|
+
key={d.path}
|
|
250
|
+
onClick={() => navigateTo(d.path)}
|
|
251
|
+
className="w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer transition-colors hover:bg-white/[0.03] group"
|
|
252
|
+
>
|
|
253
|
+
<svg className="shrink-0 text-text-3/70 group-hover:text-accent-bright/60 transition-colors" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
254
|
+
<path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2Z" />
|
|
255
|
+
</svg>
|
|
256
|
+
<span className="text-[13px] font-600 text-text-2 group-hover:text-text truncate">{d.name}</span>
|
|
257
|
+
<svg className="shrink-0 ml-auto text-text-3/50 group-hover:text-text-3/70 transition-colors" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
258
|
+
<polyline points="9 18 15 12 9 6" />
|
|
259
|
+
</svg>
|
|
260
|
+
</button>
|
|
261
|
+
))
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Actions */}
|
|
266
|
+
<div className="flex gap-3">
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => onChange(currentPath, null)}
|
|
269
|
+
className="flex-1 py-3 rounded-[14px] border border-accent-bright/20 bg-accent-soft text-accent-bright text-[14px] font-600 cursor-pointer hover:brightness-110 transition-all"
|
|
270
|
+
style={{ fontFamily: 'inherit' }}
|
|
271
|
+
>
|
|
272
|
+
Select This Directory
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
<button
|
|
276
|
+
onClick={() => setMode('native')}
|
|
277
|
+
className="text-[12px] text-text-3/60 hover:text-text-3 transition-colors cursor-pointer bg-transparent border-none p-0"
|
|
278
|
+
>
|
|
279
|
+
Or use system file picker
|
|
280
|
+
</button>
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
open: boolean
|
|
7
|
+
onClose: () => void
|
|
8
|
+
children: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Dropdown({ open, onClose, children }: Props) {
|
|
12
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!open) return
|
|
16
|
+
const handler = (e: MouseEvent) => {
|
|
17
|
+
if (ref.current && !ref.current.contains(e.target as Node)) onClose()
|
|
18
|
+
}
|
|
19
|
+
document.addEventListener('click', handler)
|
|
20
|
+
return () => document.removeEventListener('click', handler)
|
|
21
|
+
}, [open, onClose])
|
|
22
|
+
|
|
23
|
+
if (!open) return null
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
ref={ref}
|
|
28
|
+
className="fixed top-12 right-3 bg-raised border border-white/[0.06] rounded-[14px]
|
|
29
|
+
p-1.5 z-90 min-w-[200px] shadow-[0_16px_64px_rgba(0,0,0,0.6)]
|
|
30
|
+
backdrop-blur-xl"
|
|
31
|
+
style={{ animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function DropdownItem({ children, danger, onClick }: { children: ReactNode; danger?: boolean; onClick: () => void }) {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
className={`block w-full px-3.5 py-2.5 border-none bg-transparent text-[13px] font-500
|
|
43
|
+
text-left cursor-pointer rounded-[10px] transition-all duration-150
|
|
44
|
+
hover:bg-white/[0.05] active:bg-white/[0.07]
|
|
45
|
+
${danger ? 'text-danger' : 'text-text-2 hover:text-text'}`}
|
|
46
|
+
style={{ fontFamily: 'inherit' }}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</button>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function DropdownSep() {
|
|
54
|
+
return <div className="h-px bg-white/[0.04] my-1 mx-2" />
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
variant?: 'default' | 'accent' | 'danger'
|
|
8
|
+
active?: boolean
|
|
9
|
+
size?: 'sm' | 'md'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function IconButton({ children, variant = 'default', active, size = 'md', className = '', ...props }: Props) {
|
|
13
|
+
const sizeClass = size === 'sm' ? 'w-8 h-8 rounded-[9px]' : 'w-9 h-9 rounded-[10px]'
|
|
14
|
+
const base = `${sizeClass} border-none bg-transparent flex items-center justify-center cursor-pointer shrink-0 transition-all duration-200 hover:bg-white/[0.06] active:scale-90`
|
|
15
|
+
const color =
|
|
16
|
+
variant === 'accent' ? 'text-accent-bright' :
|
|
17
|
+
variant === 'danger' ? 'text-danger' :
|
|
18
|
+
active ? 'text-accent-bright bg-accent-soft' : 'text-text-3 hover:text-text-2'
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<button className={`${base} ${color} ${className}`} {...props}>
|
|
22
|
+
{children}
|
|
23
|
+
</button>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { PluginMeta, MarketplacePlugin } from '@/types'
|
|
6
|
+
|
|
7
|
+
export function PluginManager() {
|
|
8
|
+
const [tab, setTab] = useState<'installed' | 'marketplace' | 'url'>('installed')
|
|
9
|
+
const [plugins, setPlugins] = useState<PluginMeta[]>([])
|
|
10
|
+
const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
|
|
11
|
+
const [loading, setLoading] = useState(false)
|
|
12
|
+
const [installing, setInstalling] = useState<string | null>(null)
|
|
13
|
+
const [urlInput, setUrlInput] = useState('')
|
|
14
|
+
const [urlFilename, setUrlFilename] = useState('')
|
|
15
|
+
const [urlStatus, setUrlStatus] = useState<{ ok: boolean; message: string } | null>(null)
|
|
16
|
+
|
|
17
|
+
const loadPlugins = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const data = await api<PluginMeta[]>('GET', '/plugins')
|
|
20
|
+
setPlugins(data)
|
|
21
|
+
} catch { /* ignore */ }
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
const loadMarketplace = useCallback(async () => {
|
|
25
|
+
setLoading(true)
|
|
26
|
+
try {
|
|
27
|
+
const data = await api<MarketplacePlugin[]>('GET', '/plugins/marketplace')
|
|
28
|
+
if (Array.isArray(data)) setMarketplace(data)
|
|
29
|
+
} catch { /* ignore */ }
|
|
30
|
+
setLoading(false)
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
useEffect(() => { loadPlugins() }, [])
|
|
34
|
+
useEffect(() => { if (tab === 'marketplace') loadMarketplace() }, [tab])
|
|
35
|
+
|
|
36
|
+
const togglePlugin = async (filename: string, enabled: boolean) => {
|
|
37
|
+
await api('POST', '/plugins', { filename, enabled })
|
|
38
|
+
loadPlugins()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
42
|
+
setInstalling(p.id)
|
|
43
|
+
try {
|
|
44
|
+
await api('POST', '/plugins/install', { url: p.url, filename: `${p.id}.js` })
|
|
45
|
+
await loadPlugins()
|
|
46
|
+
setTab('installed')
|
|
47
|
+
} catch { /* ignore */ }
|
|
48
|
+
setInstalling(null)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const installFromUrl = async () => {
|
|
52
|
+
if (!urlInput || !urlFilename) return
|
|
53
|
+
setUrlStatus(null)
|
|
54
|
+
setInstalling('url')
|
|
55
|
+
try {
|
|
56
|
+
await api('POST', '/plugins/install', { url: urlInput, filename: urlFilename })
|
|
57
|
+
await loadPlugins()
|
|
58
|
+
setUrlStatus({ ok: true, message: 'Installed successfully' })
|
|
59
|
+
setUrlInput('')
|
|
60
|
+
setUrlFilename('')
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
setUrlStatus({ ok: false, message: err.message || 'Install failed' })
|
|
63
|
+
}
|
|
64
|
+
setInstalling(null)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const installedFilenames = new Set(plugins.map((p) => p.filename))
|
|
68
|
+
|
|
69
|
+
const tabClass = (t: string) =>
|
|
70
|
+
`py-2.5 px-4 rounded-[10px] text-center cursor-pointer transition-all text-[12px] font-600 border
|
|
71
|
+
${tab === t
|
|
72
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
73
|
+
: 'bg-bg border-white/[0.06] text-text-3 hover:bg-surface-2'}`
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<div className="flex gap-2 mb-5">
|
|
78
|
+
<button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
|
|
79
|
+
Installed{plugins.length > 0 && ` (${plugins.length})`}
|
|
80
|
+
</button>
|
|
81
|
+
<button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
|
|
82
|
+
Marketplace
|
|
83
|
+
</button>
|
|
84
|
+
<button onClick={() => setTab('url')} className={tabClass('url')} style={{ fontFamily: 'inherit' }}>
|
|
85
|
+
Install from URL
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{tab === 'installed' && (
|
|
90
|
+
plugins.length === 0
|
|
91
|
+
? <p className="text-[12px] text-text-3/70">No plugins installed</p>
|
|
92
|
+
: <div className="space-y-2.5">
|
|
93
|
+
{plugins.map((p) => (
|
|
94
|
+
<div key={p.filename} className="flex items-center gap-3 py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
95
|
+
<div className="flex-1 min-w-0">
|
|
96
|
+
<div className="flex items-center gap-2">
|
|
97
|
+
<span className="text-[14px] font-600 text-text truncate">{p.name}</span>
|
|
98
|
+
{p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
|
|
99
|
+
</div>
|
|
100
|
+
<div className="text-[11px] font-mono text-text-3 truncate">{p.filename}</div>
|
|
101
|
+
{p.description && <div className="text-[11px] text-text-3/60 mt-0.5">{p.description}</div>}
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
104
|
+
onClick={() => togglePlugin(p.filename, !p.enabled)}
|
|
105
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
106
|
+
${p.enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
107
|
+
>
|
|
108
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
109
|
+
${p.enabled ? 'left-[22px]' : 'left-0.5'}`} />
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{tab === 'marketplace' && (
|
|
117
|
+
loading
|
|
118
|
+
? <p className="text-[12px] text-text-3/70">Loading marketplace...</p>
|
|
119
|
+
: marketplace.length === 0
|
|
120
|
+
? <p className="text-[12px] text-text-3/70">No plugins available</p>
|
|
121
|
+
: <div className="space-y-2.5">
|
|
122
|
+
{marketplace.map((p) => {
|
|
123
|
+
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
124
|
+
return (
|
|
125
|
+
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
126
|
+
<div className="flex items-start gap-3">
|
|
127
|
+
<div className="flex-1 min-w-0">
|
|
128
|
+
<div className="flex items-center gap-2">
|
|
129
|
+
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
130
|
+
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
131
|
+
{p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
|
|
132
|
+
</div>
|
|
133
|
+
<div className="text-[11px] text-text-3/60 mt-1">{p.description}</div>
|
|
134
|
+
<div className="flex items-center gap-2 mt-2">
|
|
135
|
+
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
136
|
+
<span className="text-[10px] text-text-3/50">·</span>
|
|
137
|
+
{p.tags.slice(0, 3).map((t) => (
|
|
138
|
+
<span key={t} className="text-[9px] font-600 text-text-3/50 bg-white/[0.04] px-1.5 py-0.5 rounded-full">{t}</span>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => !isInstalled && installFromMarketplace(p)}
|
|
144
|
+
disabled={isInstalled || installing === p.id}
|
|
145
|
+
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
146
|
+
${isInstalled
|
|
147
|
+
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
148
|
+
: installing === p.id
|
|
149
|
+
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
150
|
+
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
151
|
+
style={{ fontFamily: 'inherit' }}
|
|
152
|
+
>
|
|
153
|
+
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{tab === 'url' && (
|
|
163
|
+
<div className="p-5 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
164
|
+
<div className="mb-4">
|
|
165
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Plugin URL</label>
|
|
166
|
+
<input
|
|
167
|
+
type="url"
|
|
168
|
+
value={urlInput}
|
|
169
|
+
onChange={(e) => setUrlInput(e.target.value)}
|
|
170
|
+
placeholder="https://example.com/my-plugin.js"
|
|
171
|
+
className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/60 outline-none focus:border-accent-bright/30"
|
|
172
|
+
style={{ fontFamily: 'inherit' }}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="mb-4">
|
|
176
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Save as filename</label>
|
|
177
|
+
<input
|
|
178
|
+
type="text"
|
|
179
|
+
value={urlFilename}
|
|
180
|
+
onChange={(e) => setUrlFilename(e.target.value)}
|
|
181
|
+
placeholder="my-plugin.js"
|
|
182
|
+
className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/60 outline-none focus:border-accent-bright/30"
|
|
183
|
+
style={{ fontFamily: 'inherit' }}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<button
|
|
187
|
+
onClick={installFromUrl}
|
|
188
|
+
disabled={!urlInput || !urlFilename || installing === 'url'}
|
|
189
|
+
className="w-full py-2.5 rounded-[10px] text-[13px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20
|
|
190
|
+
hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default"
|
|
191
|
+
style={{ fontFamily: 'inherit' }}
|
|
192
|
+
>
|
|
193
|
+
{installing === 'url' ? 'Installing...' : 'Install Plugin'}
|
|
194
|
+
</button>
|
|
195
|
+
{urlStatus && (
|
|
196
|
+
<p className={`text-[11px] mt-3 ${urlStatus.ok ? 'text-emerald-400' : 'text-red-400'}`}>
|
|
197
|
+
{urlStatus.message}
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
200
|
+
<p className="text-[10px] text-text-3/60 mt-3">
|
|
201
|
+
Works with SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
|
|
202
|
+
</p>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { SettingsSectionProps } from './types'
|
|
4
|
+
|
|
5
|
+
export function CapabilityPolicySection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="mb-10">
|
|
8
|
+
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
9
|
+
Capability Policy
|
|
10
|
+
</h3>
|
|
11
|
+
<p className="text-[12px] text-text-3 mb-5">
|
|
12
|
+
Centralized guardrails for agent tool families. Applies to direct tool calls and forced auto-routing.
|
|
13
|
+
</p>
|
|
14
|
+
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
15
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Policy Mode</label>
|
|
16
|
+
<div className="grid grid-cols-3 gap-2 mb-5">
|
|
17
|
+
{([
|
|
18
|
+
{ id: 'permissive', name: 'Permissive' },
|
|
19
|
+
{ id: 'balanced', name: 'Balanced' },
|
|
20
|
+
{ id: 'strict', name: 'Strict' },
|
|
21
|
+
] as const).map((mode) => (
|
|
22
|
+
<button
|
|
23
|
+
key={mode.id}
|
|
24
|
+
onClick={() => patchSettings({ capabilityPolicyMode: mode.id })}
|
|
25
|
+
className={`py-3 px-3 rounded-[12px] text-center cursor-pointer transition-all text-[13px] font-600 border
|
|
26
|
+
${(appSettings.capabilityPolicyMode || 'permissive') === mode.id
|
|
27
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
28
|
+
: 'bg-bg border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
29
|
+
style={{ fontFamily: 'inherit' }}
|
|
30
|
+
>
|
|
31
|
+
{mode.name}
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="grid grid-cols-1 gap-4">
|
|
37
|
+
<div>
|
|
38
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Blocked Categories</label>
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
value={(appSettings.capabilityBlockedCategories || []).join(', ')}
|
|
42
|
+
onChange={(e) => patchSettings({
|
|
43
|
+
capabilityBlockedCategories: e.target.value
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((part) => part.trim())
|
|
46
|
+
.filter(Boolean),
|
|
47
|
+
})}
|
|
48
|
+
placeholder="execution, filesystem, platform, outbound"
|
|
49
|
+
className={inputClass}
|
|
50
|
+
style={{ fontFamily: 'inherit' }}
|
|
51
|
+
/>
|
|
52
|
+
<p className="text-[11px] text-text-3/60 mt-2">Supported categories: filesystem, execution, network, browser, memory, delegation, platform, outbound.</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div>
|
|
56
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Blocked Tools</label>
|
|
57
|
+
<input
|
|
58
|
+
type="text"
|
|
59
|
+
value={(appSettings.capabilityBlockedTools || []).join(', ')}
|
|
60
|
+
onChange={(e) => patchSettings({
|
|
61
|
+
capabilityBlockedTools: e.target.value
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((part) => part.trim())
|
|
64
|
+
.filter(Boolean),
|
|
65
|
+
})}
|
|
66
|
+
placeholder="delete_file, manage_connectors, delegate_to_codex_cli"
|
|
67
|
+
className={inputClass}
|
|
68
|
+
style={{ fontFamily: 'inherit' }}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div>
|
|
73
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Allowed Tools (Override)</label>
|
|
74
|
+
<input
|
|
75
|
+
type="text"
|
|
76
|
+
value={(appSettings.capabilityAllowedTools || []).join(', ')}
|
|
77
|
+
onChange={(e) => patchSettings({
|
|
78
|
+
capabilityAllowedTools: e.target.value
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((part) => part.trim())
|
|
81
|
+
.filter(Boolean),
|
|
82
|
+
})}
|
|
83
|
+
placeholder="shell, web_fetch, browser"
|
|
84
|
+
className={inputClass}
|
|
85
|
+
style={{ fontFamily: 'inherit' }}
|
|
86
|
+
/>
|
|
87
|
+
<p className="text-[11px] text-text-3/60 mt-2">Use this to re-allow specific tool families when running in strict mode.</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|