@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.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. 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
+ }